@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.
- package/lib/index.js +87 -21
- 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
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|