@kubun/plugin-connector 0.14.2 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/index.js +87 -21
  2. package/package.json +4 -3
package/lib/index.js CHANGED
@@ -453,32 +453,98 @@ export function createConnectorPlugin(options) {
453
453
  }
454
454
  return result;
455
455
  };
456
- // Opt-in: arm periodic sync for the authenticated owner on every
457
- // `connector:authenticated`. Subscribed on `lifecycleEvents` (the merged,
458
- // tagged channel), not the per-event `events` channel forwarded to
459
- // external plugin consumers as `connectorPluginAPI.events` — this
460
- // subscriber is the connector plugin's own internal wiring, not a
461
- // consumer of its public surface, so it stays decoupled from whatever
462
- // external listeners do with `events`. `reactivate: false` so a schedule
463
- // the user explicitly disabled is not silently re-armed by a later
464
- // re-authentication; an absent schedule is still created enabled. Fire-
465
- // and-forget and best-effort: a missing workflow plugin (or any other
466
- // failure) is caught here rather than thrown into the emitter's listener,
467
- // which would otherwise only be logged and swallowed.
456
+ // Arm-if-absent recovery for one owner: for every connector this owner holds
457
+ // a credential for, create a periodic schedule when none exists yet. An
458
+ // existing schedule enabled or user-disabled is left untouched, so this
459
+ // is safe to run repeatedly and never rewrites a live schedule's `next_run`
460
+ // or re-announces an already-enabled one. It is the shared body of both the
461
+ // auth-event subscriber and the boot sweep: arming the just-authenticated
462
+ // connectors and retrying any earlier arm that failed transiently for the
463
+ // same owner. Failures are isolated per connector and never propagate.
464
+ const reconcileOwnerNow = async (ownerDID)=>{
465
+ const workflowAPI = await getWorkflowAPI();
466
+ if (workflowAPI == null) return;
467
+ for (const connector of registry.getAll()){
468
+ if (connector.auth.provider === 'device') continue;
469
+ try {
470
+ const credential = await backgroundCredentialProvider.get(connector.auth.provider, ownerDID);
471
+ if (credential == null) continue;
472
+ const { subjectKey } = periodicSyncSubject(connector.name, ownerDID);
473
+ const existing = await workflowAPI.getPeriodicSync(`${CONNECTOR_SYNC_WORKFLOW}:${subjectKey}`);
474
+ if (existing != null) continue;
475
+ await enablePeriodicSyncForOwner(connector.name, ownerDID, undefined, {
476
+ reactivate: false
477
+ });
478
+ } catch (err) {
479
+ logger.debug('auto-arm reconcile skipped {connector} {ownerDID}', {
480
+ connector: connector.name,
481
+ ownerDID,
482
+ error: err instanceof Error ? err.message : String(err)
483
+ });
484
+ }
485
+ }
486
+ };
487
+ // Serialize reconciles per owner. The arm-if-absent check (getPeriodicSync)
488
+ // and the arm are two steps, so two reconciles of the same owner running
489
+ // concurrently — the boot sweep racing an auth event, or two auth events in
490
+ // quick succession — could both read a subject as absent and both arm it. The
491
+ // atomic upsert stops that becoming a PK error, but the loser would still
492
+ // rewrite `next_run` and emit a second `periodicSync:enabled`, breaking the
493
+ // no-touch guarantee above. Chaining per normalized owner makes the later run
494
+ // observe the schedule the earlier one created, so it skips.
495
+ const reconcileChains = new Map();
496
+ const reconcileOwner = (ownerDID)=>{
497
+ const key = normalizeDID(ownerDID);
498
+ const prev = reconcileChains.get(key) ?? Promise.resolve();
499
+ const next = prev.catch(()=>{}).then(()=>reconcileOwnerNow(ownerDID));
500
+ reconcileChains.set(key, next);
501
+ void next.finally(()=>{
502
+ // Only the tail clears the entry; a newer chained run keeps it alive.
503
+ if (reconcileChains.get(key) === next) reconcileChains.delete(key);
504
+ });
505
+ return next;
506
+ };
507
+ // Opt-in: reconcile the authenticated owner on every `connector:authenticated`.
508
+ // Subscribed on `lifecycleEvents` (the merged, tagged channel), not the
509
+ // per-event `events` channel forwarded to external plugin consumers as
510
+ // `connectorPluginAPI.events` — this subscriber is the connector plugin's own
511
+ // internal wiring, not a consumer of its public surface, so it stays decoupled
512
+ // from whatever external listeners do with `events`. Reconciling the whole
513
+ // owner (not just `event.connectors`) means a connector whose arm failed at an
514
+ // earlier auth is recovered the next time the owner authenticates anything.
515
+ // Fire-and-forget and best-effort: any failure is caught inside
516
+ // `reconcileOwner` rather than thrown into the emitter's listener.
468
517
  if (options.autoEnablePeriodicSync) {
469
518
  manager.lifecycleEvents.on('lifecycle', (event)=>{
470
519
  if (event.type !== 'connector:authenticated') return;
471
- for (const connector of event.connectors){
472
- enablePeriodicSyncForOwner(connector, event.ownerDID, undefined, {
473
- reactivate: false
474
- }).catch((err)=>{
475
- logger.debug('auto-enable periodic sync skipped {connector} {ownerDID}', {
476
- connector,
477
- ownerDID: event.ownerDID,
478
- error: err instanceof Error ? err.message : String(err)
479
- });
520
+ reconcileOwner(event.ownerDID).catch((err)=>{
521
+ logger.debug('auto-arm reconcile (on auth) failed {ownerDID}', {
522
+ ownerDID: event.ownerDID,
523
+ error: err instanceof Error ? err.message : String(err)
480
524
  });
525
+ });
526
+ });
527
+ // Boot sweep (standalone only): on init, re-arm any owner authenticated in a
528
+ // previous run whose schedule is missing — the restart/mount recovery for a
529
+ // transient arm failure that no later auth would otherwise reach. It runs
530
+ // once, off the request path, so it is fire-and-forget and must not block or
531
+ // fail init. Service mode has no local credential store to enumerate and its
532
+ // remote backend owns its own reconcile, so the sweep is skipped there; the
533
+ // auth-event path above still recovers in both modes.
534
+ void (async ()=>{
535
+ const serviceProvider = await credentialServiceProviderPromise;
536
+ if (serviceProvider != null) return;
537
+ const { api } = getStandaloneStack(params.db);
538
+ const providers = new Set(registry.getAll().map((connector)=>connector.auth.provider).filter((provider)=>provider !== 'device'));
539
+ const owners = new Set();
540
+ for (const provider of providers){
541
+ for (const owner of (await api.listSubjects(provider)))owners.add(owner);
481
542
  }
543
+ for (const owner of owners)await reconcileOwner(owner);
544
+ })().catch((err)=>{
545
+ logger.debug('auto-arm boot sweep failed {error}', {
546
+ error: err instanceof Error ? err.message : String(err)
547
+ });
482
548
  });
483
549
  }
484
550
  // Process-local dedup for the no-workflow-plugin fallback path only (the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/plugin-connector",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "license": "see LICENSE.md",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "@kokuin/jwe": "^0.1.0",
19
19
  "@kokuin/token": "^0.5.0",
20
20
  "@kubun/connector": "^0.14.0",
21
- "@kubun/credential": "^0.14.2",
21
+ "@kubun/credential": "^0.14.3",
22
22
  "@kubun/credential-types": "^0.14.0",
23
23
  "@kubun/db": "^0.14.0",
24
24
  "@kubun/engine": "^0.14.1",
@@ -31,7 +31,7 @@
31
31
  "@kubun/protocol": "^0.14.1",
32
32
  "@kubun/service-credential-api": "^0.14.0",
33
33
  "@kubun/store-connector": "^0.14.0",
34
- "@kubun/store-credential": "^0.14.0",
34
+ "@kubun/store-credential": "^0.14.1",
35
35
  "@kubun/store-delegation": "^0.14.0",
36
36
  "@kubun/store-graph": "^0.14.1",
37
37
  "@noble/hashes": "^2.4.0",
@@ -50,6 +50,7 @@
50
50
  "@kokuin/controller": "^0.1.0",
51
51
  "@kubun/blob-backend": "^0.14.0",
52
52
  "@kubun/connector-google-mail": "^0.14.0",
53
+ "@kubun/db-node-sqlite": "^0.14.0",
53
54
  "@kubun/db-postgres": "^0.14.0",
54
55
  "@kubun/models": "^0.14.0",
55
56
  "@kubun/plugin-blob": "^0.14.0",