Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Create a [feature request](https://github.com/ConductionNL/filinq/issues/new)

**Support:** For support, contact support@conduction.nl. For a Service Level Agreement (SLA), contact sales@conduction.nl.
]]></description>
<version>0.1.6-unstable.20260831045800</version>
<version>0.1.9-unstable.20260831212416</version>
<licence>EUPL-1.2</licence>
<author mail="info@conduction.nl" homepage="https://www.conduction.nl/">Conduction</author>
<namespace>Filinq</namespace>
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@
"optimize-autoloader": true,
"sort-packages": true,
"platform": {
"php": "8.3"
"php": "8.3",
"ext-xsl": "1"
},
"preferred-install": "dist",
"process-timeout": 600
Expand Down
142 changes: 134 additions & 8 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,32 @@ export default [
'no-console': 'off',
'n/no-process-exit': 'off',
'n/hashbang': 'off',
// `_` / `__` as a deliberate throwaway binding — `catch (_)`, a
// discarded destructuring slot. Narrow on purpose: the pattern matches
// UNDERSCORES ONLY, so a real name that happens to start with `_` is
// still reported. v9 drives plain `.js` through the CORE rule (the
// `@typescript-eslint` swap is per-file-type), so it is set here.
// Tests import devDependencies by definition; this rule is about what
// ships in the published package, which tests/ never does.
'n/no-unpublished-import': 'off',
},
},

{
// `_` / `__` as a deliberate throwaway binding — `catch (_)`, a discarded
// destructuring slot. Narrow on purpose: the pattern matches UNDERSCORES
// ONLY, so a real name that happens to start with `_` is still reported.
//
// 🔴 `.js` / `.mjs` ONLY, NOT `.ts`. The CORE rule is not TypeScript-aware:
// applied to a `.ts` file it reads the parameter NAMES inside a function
// TYPE as bindings and reports them unused. Measured on humaniq —
//
// t?: (app: string, key: string) => string
//
// produced four `no-unused-vars` errors for `app` and `key`, which are
// documentation, not variables. The same mis-scoping made every unused
// `catch (e)` in a `.ts` spec report TWICE, once per rule.
//
// v9 already turns the core rule off for `.ts` and drives
// `@typescript-eslint/no-unused-vars` instead; naming `.ts` here switched
// it back on. TypeScript files are handled by the block below.
files: ['tests/**/*.js', 'tests/**/*.mjs'],
rules: {
'no-unused-vars': [
'error',
{
Expand All @@ -165,12 +186,117 @@ export default [
ignoreRestSiblings: true,
},
],
// Tests import devDependencies by definition; this rule is about what
// ships in the published package, which tests/ never does.
'n/no-unpublished-import': 'off',
},
},

{
// The TypeScript half of the block above. Same intent, same patterns, on
// the rule that actually understands the language: it knows a name inside
// a function type is not a binding, so type annotations stay quiet while a
// genuinely dead local is still reported.
files: ['tests/**/*.ts', 'tests/**/*.tsx'],
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
varsIgnorePattern: '^_+$',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_+$',
argsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
},
},

{
// 🔴 Node-side CLI tooling under `scripts/`, which is COMMONJS. Flat
// config defaults every `.js` to ESM with browser-ish globals, so without
// this block eslint reports the CommonJS wrapper itself as undefined
// identifiers. Measured on this app: 52 of the 233 errors under
// `tests/` + `scripts/` were `no-undef`, ALL of them in `scripts/`, and
// all five names were the environment rather than a typo — `process` 23,
// `require` 20, `__dirname` 6, `__filename` 2, `module` 1.
//
// This is describing the environment, not relaxing a rule, and it is the
// same argument the test-globals block below makes: declaring them keeps
// `no-undef` able to do its real job, which is catching a genuinely
// misspelled identifier. Suppressing the rule instead would bury that.
//
// `no-console` is off because printing its report is what a CLI checker
// is FOR.
//
// 🔴 NO `n/*` ENTRIES HERE, DELIBERATELY. `eslint-plugin-n` is NOT
// registered for these files under eslint 10 + @nextcloud/eslint-config
// 9, so `'n/no-process-exit': 'off'` would be dead config that reads as
// if it were doing something. Measured both ways on this app: 0 `n/`
// findings with the entries and 0 without.
//
// What DID report was the opposite — four `scripts/*.js` carried
// `/* eslint-disable n/no-process-exit */` and `/* eslint-disable
// n/shebang */` left over from the eslintrc era, and an inline disable
// naming an unregistered plugin is itself an error ("Definition for rule
// 'n/shebang' was not found"). Those 8 comments are removed; do not add
// `n/*` rules back to replace them.
//
// ⚠️ `.js` and `.cjs` ONLY. A `scripts/*.mjs` is genuinely ESM and must
// keep the default `sourceType`, or `import` stops parsing there.
files: ['scripts/**/*.js', 'scripts/**/*.cjs'],
languageOptions: {
sourceType: 'commonjs',
globals: {
require: 'readonly',
module: 'writable',
exports: 'writable',
process: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
console: 'readonly',
Buffer: 'readonly',
global: 'readonly',
URL: 'readonly',
TextEncoder: 'readonly',
TextDecoder: 'readonly',
},
},
rules: {
'no-console': 'off',
},
},

{
// The ESM half of the block above. A `scripts/*.mjs` is genuinely a module
// and must keep the default `sourceType`, so it gets Node's globals but
// none of the CommonJS wrapper. Measured: `process` reported undefined 2x
// in hermiq's generate-opengemeenten-icons.mjs and 4x in openregister's
// l10n/runtime-check.mjs, which the `.js`/`.cjs` block deliberately does
// not match.
files: ['scripts/**/*.mjs', 'tests/**/*.mjs'],
languageOptions: {
globals: {
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
global: 'readonly',
URL: 'readonly',
TextEncoder: 'readonly',
TextDecoder: 'readonly',
},
},
rules: {
'no-console': 'off',
},
},

{
// eslint must not try to PARSE a shell script. `tests/e2e/seed.test.sh`
// matches the `**/*.test.*` glob some presets use, and eslint then reads
// it as JavaScript and reports "Parsing error: Unexpected character" —
// a finding about a file it should never have opened.
ignores: ['**/*.sh', '**/*.bash'],
},


{
// Test globals. Several apps keep their spec files INSIDE `src/`, which the
// lint script scans, and neither `@nextcloud/eslint-config` nor the runner
Expand Down
58 changes: 39 additions & 19 deletions lib/AppInfo/SigningEventRegistrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Filinq Signing Event Registrar
*
* Wires the signing-related event listeners: the bridge from OpenRegister's
* ApprovalStep events into Filinq's typed Signer* events, and the cross-app
* task-sequence events into Filinq's typed Signer* events, and the cross-app
* delegated-signing request contract. Extracted from `Application`.
*
* @category AppInfo
Expand All @@ -26,45 +26,65 @@
namespace OCA\Filinq\AppInfo;

use OCA\Filinq\Event\DocumentSigningRequestedEvent;
use OCA\Filinq\EventListener\ApprovalStepListener;
use OCA\Filinq\EventListener\DocumentSigningRequestedListener;
use OCA\OpenRegister\Event\ApprovalStepApprovedEvent;
use OCA\OpenRegister\Event\ApprovalStepCompletedEvent;
use OCA\OpenRegister\Event\ApprovalStepInitiatedEvent;
use OCA\OpenRegister\Event\ApprovalStepRejectedEvent;
use OCA\Filinq\EventListener\SigningTaskListener;
use OCP\AppFramework\Bootstrap\IRegistrationContext;

/**
* Registers the approval-step bridge and the cross-app signing-request listener.
* Registers the task-sequence bridge and the cross-app signing-request listener.
*
* @category AppInfo
* @package OCA\Filinq\AppInfo
* @author Conduction B.V. <info@conduction.nl>
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
* @link https://www.filinq.app
*
* @spec openspec/specs/signing-via-or-approval-with-provider-plugins/spec.md
*/
class SigningEventRegistrar {

/**
* The OpenRegister task events the signing bridge consumes, as FQN
* string literals on purpose. `::class` on an imported name is a
* compile-time string too, but a literal keeps that true even if
* someone later adds the import — and during our own register() the
* `OCA\OpenRegister\` prefix is not on the autoloader yet, so neither a
* `class_exists()` probe (always false here) nor an eager reference
* (aborts register()) is an option; `BootstrapOrderIndependenceTest`
* pins both rules. Registering for an event class that never comes to
* exist is harmless: the dispatcher keys listeners by name, and the
* name is simply never dispatched. Mapping per openregister#3302
* (flow-approval-consolidation, approval-events-migration.md):
* transitioned-to-enabled replaces the retired step-initiated signal,
* committed terminality replaces step-approved and step-rejected, and
* sequence completion replaces chain completion.
*
* @var array<int, string>
*/
public const TASK_EVENTS = [
'OCA\\OpenRegister\\Event\\TaskTransitionedEvent',
'OCA\\OpenRegister\\Event\\TaskTerminalEvent',
'OCA\\OpenRegister\\Event\\TaskSequenceCompletedEvent',
];

/**
* Register the signing event listeners.
*
* @param IRegistrationContext $context The registration context.
*
* @return void
*
* @spec openspec/specs/document-signing/spec.md
* @spec openspec/specs/signing-via-or-approval-with-provider-plugins/spec.md
*/
public function register(IRegistrationContext $context): void {
// Bridge OR ApprovalStep events into typed filinq Signer*Events
// and invoke the configured SigningProviderInterface when a step
// becomes pending. Per migrate-signing-to-or-approval-workflow
// (D2.1) — OR's `add-approval-step-events` shipped upstream as of
// 2026-06-12 so the four event classes referenced below resolve at
// runtime; if the OR app is absent (degraded install) the listener
// simply never receives the events.
$context->registerEventListener(ApprovalStepInitiatedEvent::class, ApprovalStepListener::class);
$context->registerEventListener(ApprovalStepApprovedEvent::class, ApprovalStepListener::class);
$context->registerEventListener(ApprovalStepRejectedEvent::class, ApprovalStepListener::class);
$context->registerEventListener(ApprovalStepCompletedEvent::class, ApprovalStepListener::class);
// Bridge OR's task-sequence events into typed filinq Signer*Events
// and invoke the configured SigningProviderInterface when a sequence
// position becomes enabled. If the OR app is absent (degraded
// install) or predates the task surface, the listener simply never
// receives the events.
foreach (self::TASK_EVENTS as $taskEvent) {
$context->registerEventListener(event: $taskEvent, listener: SigningTaskListener::class);
}

// Cross-app delegated-signing contract (filinq-signing-events): any
// installed consumer app (e.g. shillinq) dispatches
Expand Down
Loading
Loading