@krischoichoi/channel-control 0.6.0

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 (82) hide show
  1. package/README.md +67 -0
  2. package/lib/access/conversation-directory-store.d.ts +73 -0
  3. package/lib/access/conversation-directory-store.d.ts.map +1 -0
  4. package/lib/access/conversation-directory-store.js +66 -0
  5. package/lib/access/conversation-directory-store.js.map +1 -0
  6. package/lib/access/conversation-directory.d.ts +51 -0
  7. package/lib/access/conversation-directory.d.ts.map +1 -0
  8. package/lib/access/conversation-directory.js +118 -0
  9. package/lib/access/conversation-directory.js.map +1 -0
  10. package/lib/access/manager.d.ts +47 -0
  11. package/lib/access/manager.d.ts.map +1 -0
  12. package/lib/access/manager.js +124 -0
  13. package/lib/access/manager.js.map +1 -0
  14. package/lib/access/materialize.d.ts +33 -0
  15. package/lib/access/materialize.d.ts.map +1 -0
  16. package/lib/access/materialize.js +67 -0
  17. package/lib/access/materialize.js.map +1 -0
  18. package/lib/access/owner-claim.d.ts +91 -0
  19. package/lib/access/owner-claim.d.ts.map +1 -0
  20. package/lib/access/owner-claim.js +200 -0
  21. package/lib/access/owner-claim.js.map +1 -0
  22. package/lib/access/policy-store.d.ts +55 -0
  23. package/lib/access/policy-store.d.ts.map +1 -0
  24. package/lib/access/policy-store.js +88 -0
  25. package/lib/access/policy-store.js.map +1 -0
  26. package/lib/access/validation.d.ts +29 -0
  27. package/lib/access/validation.d.ts.map +1 -0
  28. package/lib/access/validation.js +89 -0
  29. package/lib/access/validation.js.map +1 -0
  30. package/lib/auth/sanitizer.d.ts +35 -0
  31. package/lib/auth/sanitizer.d.ts.map +1 -0
  32. package/lib/auth/sanitizer.js +52 -0
  33. package/lib/auth/sanitizer.js.map +1 -0
  34. package/lib/auth/session-manager.d.ts +38 -0
  35. package/lib/auth/session-manager.d.ts.map +1 -0
  36. package/lib/auth/session-manager.js +219 -0
  37. package/lib/auth/session-manager.js.map +1 -0
  38. package/lib/config.d.ts +20 -0
  39. package/lib/config.d.ts.map +1 -0
  40. package/lib/config.js +15 -0
  41. package/lib/config.js.map +1 -0
  42. package/lib/credentials/manager.d.ts +44 -0
  43. package/lib/credentials/manager.d.ts.map +1 -0
  44. package/lib/credentials/manager.js +24 -0
  45. package/lib/credentials/manager.js.map +1 -0
  46. package/lib/definitions/registry.d.ts +24 -0
  47. package/lib/definitions/registry.d.ts.map +1 -0
  48. package/lib/definitions/registry.js +59 -0
  49. package/lib/definitions/registry.js.map +1 -0
  50. package/lib/errors.d.ts +20 -0
  51. package/lib/errors.d.ts.map +1 -0
  52. package/lib/errors.js +45 -0
  53. package/lib/errors.js.map +1 -0
  54. package/lib/index.d.ts +31 -0
  55. package/lib/index.d.ts.map +1 -0
  56. package/lib/index.js +31 -0
  57. package/lib/index.js.map +1 -0
  58. package/lib/plugin.d.ts +20 -0
  59. package/lib/plugin.d.ts.map +1 -0
  60. package/lib/plugin.js +81 -0
  61. package/lib/plugin.js.map +1 -0
  62. package/lib/runtime/manager.d.ts +73 -0
  63. package/lib/runtime/manager.d.ts.map +1 -0
  64. package/lib/runtime/manager.js +277 -0
  65. package/lib/runtime/manager.js.map +1 -0
  66. package/lib/runtime/mount-handle.d.ts +11 -0
  67. package/lib/runtime/mount-handle.d.ts.map +1 -0
  68. package/lib/runtime/mount-handle.js +2 -0
  69. package/lib/runtime/mount-handle.js.map +1 -0
  70. package/lib/service.d.ts +180 -0
  71. package/lib/service.d.ts.map +1 -0
  72. package/lib/service.js +432 -0
  73. package/lib/service.js.map +1 -0
  74. package/lib/types.d.ts +370 -0
  75. package/lib/types.d.ts.map +1 -0
  76. package/lib/types.js +2 -0
  77. package/lib/types.js.map +1 -0
  78. package/lib/update-check.d.ts +128 -0
  79. package/lib/update-check.d.ts.map +1 -0
  80. package/lib/update-check.js +380 -0
  81. package/lib/update-check.js.map +1 -0
  82. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # @krischoichoi/channel-control
2
+
3
+ Universal Channel Control Plane for DeepSeek Harness Channels.
4
+
5
+ Mounts a Cordis service at `ctx.channelControl` that owns channel configuration,
6
+ credentials, interactive auth sessions and runtime lifecycle. It is
7
+ adapter-agnostic: every channel is driven through its `ChannelDefinition`, never
8
+ through per-channel conditional code.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pnpm add @krischoichoi/channel-control
14
+ ```
15
+
16
+ As a Cordis plugin:
17
+
18
+ ```yaml
19
+ - id: channels-control
20
+ name: '@krischoichoi/channel-control/plugin'
21
+ inject:
22
+ - channels
23
+ - credentials
24
+ ```
25
+
26
+ ## What it does
27
+
28
+ | Manager | Responsibility |
29
+ | --- | --- |
30
+ | `definitions` (`ChannelDefinitionRegistry`) | Holds one `ChannelDefinition` per channel, registered by channel plugins |
31
+ | `credentials` (`CredentialManager`) | Thin structural seam over `ctx.credentials`; secret values are never returned |
32
+ | `auth` (`AuthSessionManager`) | Drives QR/credential auth sessions (`beginAuth`, `pollAuth`, `submitAuthInput`, `cancelAuth`) |
33
+ | `runtime` (`ChannelRuntimeManager`) | Mounts/starts/stops/restarts adapter instances and tracks health |
34
+
35
+ The service auto-starts configured channels when their definition is registered,
36
+ and stops all mounts when the owning Cordis fiber unloads. A single
37
+ misconfigured channel never crashes profile activation.
38
+
39
+ ## API surface
40
+
41
+ ```ts
42
+ ctx.channelControl.listChannels(); // ChannelSummary[]
43
+ ctx.channelControl.getSetup('qq'); // setup descriptor
44
+ ctx.channelControl.getConfiguredState('qq');
45
+ ctx.channelControl.saveConfig('qq', patch); // rejects secret fields
46
+ ctx.channelControl.saveCredential('qq', 'appSecret', value);
47
+ ctx.channelControl.applySetup('qq', input); // transactional setup + reconcile
48
+ ctx.channelControl.beginAuth('weixin', input);
49
+ ctx.channelControl.pollAuth(sessionId);
50
+ ```
51
+
52
+ ## Development
53
+
54
+ ```bash
55
+ pnpm --filter @krischoichoi/channel-control build
56
+ pnpm --filter @krischoichoi/channel-control typecheck
57
+ pnpm --filter @krischoichoi/channel-control test
58
+ ```
59
+
60
+ ## Related
61
+
62
+ - [Repository root](../../README.md)
63
+ - [Architecture design](../../docs/architecture.md)
64
+
65
+ ## License
66
+
67
+ [MIT](../../LICENSE)
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Conversation Identity Directory — durable store for the
3
+ * discovered canonical conversation identities plus optional display metadata.
4
+ *
5
+ * This is identity METADATA only (plan §13). It must never contain message
6
+ * content, attachments, raw events, secrets, or any authorization state: the
7
+ * Access Gate keeps exact-matching `conversation.id`; the directory only lets
8
+ * the Web choose previously observed group OpenIDs for policy authoring.
9
+ *
10
+ * Records are keyed per `channelId + accountId` (plan §24/§25), so identities
11
+ * observed under account A can never leak into account B.
12
+ */
13
+ import type { ChannelStorage } from '@krischoichoi/channel-core';
14
+ /** One observed conversation identity (plan §6). */
15
+ export interface ConversationIdentityEntry {
16
+ channelId: string;
17
+ accountId: string;
18
+ /**
19
+ * Runtime canonical conversation id (QQ group: `group_openid`) — the only
20
+ * authorization key.
21
+ */
22
+ canonicalId: string;
23
+ type: 'group';
24
+ /**
25
+ * Optional human/platform-facing identifier. Display metadata only — never
26
+ * an authorization key.
27
+ */
28
+ externalId?: string;
29
+ /** Human-readable title observed by the adapter, when the platform provides one. */
30
+ displayName?: string;
31
+ /** Local, operator-facing alias. */
32
+ alias?: string;
33
+ /** Opaque non-secret provider/application scope; never part of the Web DTO. */
34
+ scopeFingerprint?: string;
35
+ /**
36
+ * Set when the same canonicalId was later observed with a DIFFERENT
37
+ * externalId (plan §23). The latest externalId is kept and the conflict is
38
+ * surfaced for re-confirmation; authorization is never changed by it.
39
+ */
40
+ identityConflict?: boolean;
41
+ firstSeenAt: number;
42
+ lastSeenAt: number;
43
+ }
44
+ /** Stored record shape under one channel+account (versioned, plan §25). */
45
+ export interface ConversationDirectoryRecord {
46
+ version: 1;
47
+ conversations: Record<string, ConversationIdentityEntry>;
48
+ }
49
+ /** Durable conversation-directory store abstraction. */
50
+ export interface ConversationDirectoryStore {
51
+ get(channelId: string, accountId: string): Promise<ConversationDirectoryRecord | undefined>;
52
+ set(channelId: string, accountId: string, record: ConversationDirectoryRecord): Promise<void>;
53
+ }
54
+ /**
55
+ * Storage key for one channel+account's directory record. Scoped to the
56
+ * channel-control domain — consumers outside this package (web) go through the
57
+ * control API, never this key format.
58
+ */
59
+ export declare function conversationDirectoryStorageKey(channelId: string, accountId: string): string;
60
+ /** Durable store backed by the shared channel-domain [ChannelStorage]. */
61
+ export declare class ChannelStorageConversationDirectoryStore implements ConversationDirectoryStore {
62
+ private readonly getStorage;
63
+ constructor(getStorage: () => ChannelStorage);
64
+ get(channelId: string, accountId: string): Promise<ConversationDirectoryRecord | undefined>;
65
+ set(channelId: string, accountId: string, record: ConversationDirectoryRecord): Promise<void>;
66
+ }
67
+ /** In-memory store (tests / transient default); state is lost on restart. */
68
+ export declare class MemoryConversationDirectoryStore implements ConversationDirectoryStore {
69
+ private readonly values;
70
+ get(channelId: string, accountId: string): Promise<ConversationDirectoryRecord | undefined>;
71
+ set(channelId: string, accountId: string, record: ConversationDirectoryRecord): Promise<void>;
72
+ }
73
+ //# sourceMappingURL=conversation-directory-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-directory-store.d.ts","sourceRoot":"","sources":["../../src/access/conversation-directory-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGjE,oDAAoD;AACpD,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,2EAA2E;AAC3E,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,CAAC,CAAC;IACX,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;CAC1D;AAkCD,wDAAwD;AACxD,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,GAAG,SAAS,CAAC,CAAC;IAC5F,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/F;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5F;AAED,0EAA0E;AAC1E,qBAAa,wCAAyC,YAAW,0BAA0B;IACzF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;gBAEtC,UAAU,EAAE,MAAM,cAAc;IAItC,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,GAAG,SAAS,CAAC;IAM3F,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;CAMpG;AAED,6EAA6E;AAC7E,qBAAa,gCAAiC,YAAW,0BAA0B;IACjF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkD;IAEnE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,GAAG,SAAS,CAAC;IAI3F,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;CAGpG"}
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ const entrySchema = z.object({
3
+ channelId: z.string().min(1),
4
+ accountId: z.string().min(1),
5
+ canonicalId: z.string().min(1),
6
+ type: z.literal('group'),
7
+ externalId: z.string().min(1).optional(),
8
+ displayName: z.string().min(1).optional(),
9
+ alias: z.string().min(1).optional(),
10
+ scopeFingerprint: z.string().min(1).optional(),
11
+ identityConflict: z.boolean().optional(),
12
+ firstSeenAt: z.number(),
13
+ lastSeenAt: z.number(),
14
+ }).loose();
15
+ const recordSchema = z.object({
16
+ version: z.literal(1),
17
+ conversations: z.record(z.string().min(1), entrySchema),
18
+ }, {
19
+ error: 'conversation directory record must be a v1 record',
20
+ });
21
+ function parseRecord(raw) {
22
+ let value;
23
+ try {
24
+ value = JSON.parse(raw);
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ const parsed = recordSchema.safeParse(value);
30
+ return parsed.success ? parsed.data : undefined;
31
+ }
32
+ /**
33
+ * Storage key for one channel+account's directory record. Scoped to the
34
+ * channel-control domain — consumers outside this package (web) go through the
35
+ * control API, never this key format.
36
+ */
37
+ export function conversationDirectoryStorageKey(channelId, accountId) {
38
+ return `conversation-directory:v1:${encodeURIComponent(channelId)}:${encodeURIComponent(accountId)}`;
39
+ }
40
+ /** Durable store backed by the shared channel-domain [ChannelStorage]. */
41
+ export class ChannelStorageConversationDirectoryStore {
42
+ getStorage;
43
+ constructor(getStorage) {
44
+ this.getStorage = getStorage;
45
+ }
46
+ async get(channelId, accountId) {
47
+ const raw = await this.getStorage().get(conversationDirectoryStorageKey(channelId, accountId));
48
+ if (raw === undefined)
49
+ return undefined;
50
+ return parseRecord(raw);
51
+ }
52
+ async set(channelId, accountId, record) {
53
+ await this.getStorage().set(conversationDirectoryStorageKey(channelId, accountId), JSON.stringify(record));
54
+ }
55
+ }
56
+ /** In-memory store (tests / transient default); state is lost on restart. */
57
+ export class MemoryConversationDirectoryStore {
58
+ values = new Map();
59
+ async get(channelId, accountId) {
60
+ return this.values.get(conversationDirectoryStorageKey(channelId, accountId));
61
+ }
62
+ async set(channelId, accountId, record) {
63
+ this.values.set(conversationDirectoryStorageKey(channelId, accountId), record);
64
+ }
65
+ }
66
+ //# sourceMappingURL=conversation-directory-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-directory-store.js","sourceRoot":"","sources":["../../src/access/conversation-directory-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * ConversationDirectory — observes canonical channel events and maintains the
3
+ * human identifier ↔ canonical conversation id mapping (plan §12).
4
+ *
5
+ * The observer consumes ONLY the canonical event surface
6
+ * (`conversation.id` / `conversation.type` / `conversation.externalId` /
7
+ * `conversation.name`) — it never sees or parses a platform raw payload. The
8
+ * adapter is responsible for having validated `externalId` at its trust
9
+ * boundary.
10
+ *
11
+ * Authorization is untouched by everything in this file: the directory stores
12
+ * identity metadata, and the Access Gate keeps exact-matching the canonical
13
+ * `conversation.id`.
14
+ */
15
+ import type { ChannelEvent, ChannelLogger } from '@krischoichoi/channel-core';
16
+ import type { PublicConversationIdentity } from '../types.js';
17
+ import type { ConversationDirectoryStore, ConversationIdentityEntry } from './conversation-directory-store.js';
18
+ /** Project a stored entry onto the sanitized Web DTO (drops scope bookkeeping). */
19
+ export declare function toPublicConversationIdentity(entry: ConversationIdentityEntry): PublicConversationIdentity;
20
+ export interface ConversationDirectoryOptions {
21
+ store: ConversationDirectoryStore;
22
+ /** Injectable clock (ms since epoch). Defaults to Date.now. */
23
+ now?: () => number;
24
+ logger?: Pick<ChannelLogger, 'warn'>;
25
+ }
26
+ export declare class ConversationDirectory {
27
+ private readonly store;
28
+ private readonly now;
29
+ private readonly logger;
30
+ /** Serializes read-modify-write cycles per channel+account. */
31
+ private readonly chains;
32
+ constructor(options: ConversationDirectoryOptions);
33
+ /**
34
+ * Observe one canonical event. Every group message creates or refreshes its
35
+ * canonical identity; `externalId`, when a platform actually provides one,
36
+ * is optional display metadata only.
37
+ * Never throws — a directory failure must not break the inbound loop.
38
+ */
39
+ observe(event: ChannelEvent, scopeFingerprint?: string, scopeRequired?: boolean): void;
40
+ /** All observed conversation identities for one channel+account. */
41
+ list(channelId: string, accountId?: string, scopeFingerprint?: string, scopeRequired?: boolean): Promise<ConversationIdentityEntry[]>;
42
+ /**
43
+ * Resolve an optional platform-facing identifier to an observed canonical
44
+ * entry. QQ does not use this path: its Web UI selects discovered canonical
45
+ * group OpenIDs directly.
46
+ */
47
+ resolveByExternalId(channelId: string, externalId: string | undefined, accountId?: string, scopeFingerprint?: string, scopeRequired?: boolean): Promise<ConversationIdentityEntry | undefined>;
48
+ /** Read-modify-write one mapping under the channel+account scope. */
49
+ private record;
50
+ }
51
+ //# sourceMappingURL=conversation-directory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-directory.d.ts","sourceRoot":"","sources":["../../src/access/conversation-directory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EACV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,mCAAmC,CAAC;AAE3C,mFAAmF;AACnF,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,yBAAyB,GAC/B,0BAA0B,CAW5B;AAED,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,0BAA0B,CAAC;IAClC,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IACrD,+DAA+D;IAC/D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;gBAE/C,OAAO,EAAE,4BAA4B;IAMjD;;;;;OAKG;IACH,OAAO,CACL,KAAK,EAAE,YAAY,EACnB,gBAAgB,CAAC,EAAE,MAAM,EACzB,aAAa,UAAQ,GACpB,IAAI;IA+BP,oEAAoE;IAC9D,IAAI,CACR,SAAS,EAAE,MAAM,EACjB,SAAS,SAAS,EAClB,gBAAgB,CAAC,EAAE,MAAM,EACzB,aAAa,UAAQ,GACpB,OAAO,CAAC,yBAAyB,EAAE,CAAC;IAevC;;;;OAIG;IACG,mBAAmB,CACvB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,SAAS,SAAS,EAClB,gBAAgB,CAAC,EAAE,MAAM,EACzB,aAAa,UAAQ,GACpB,OAAO,CAAC,yBAAyB,GAAG,SAAS,CAAC;IAUjD,qEAAqE;YACvD,MAAM;CA6CrB"}
@@ -0,0 +1,118 @@
1
+ /** Project a stored entry onto the sanitized Web DTO (drops scope bookkeeping). */
2
+ export function toPublicConversationIdentity(entry) {
3
+ return {
4
+ type: entry.type,
5
+ canonicalId: entry.canonicalId,
6
+ ...(entry.externalId ? { externalId: entry.externalId } : {}),
7
+ ...(entry.displayName ? { displayName: entry.displayName } : {}),
8
+ ...(entry.alias ? { alias: entry.alias } : {}),
9
+ ...(entry.identityConflict ? { identityConflict: true } : {}),
10
+ firstSeenAt: entry.firstSeenAt,
11
+ lastSeenAt: entry.lastSeenAt,
12
+ };
13
+ }
14
+ export class ConversationDirectory {
15
+ store;
16
+ now;
17
+ logger;
18
+ /** Serializes read-modify-write cycles per channel+account. */
19
+ chains = new Map();
20
+ constructor(options) {
21
+ this.store = options.store;
22
+ this.now = options.now ?? Date.now;
23
+ this.logger = options.logger ?? { warn: () => { } };
24
+ }
25
+ /**
26
+ * Observe one canonical event. Every group message creates or refreshes its
27
+ * canonical identity; `externalId`, when a platform actually provides one,
28
+ * is optional display metadata only.
29
+ * Never throws — a directory failure must not break the inbound loop.
30
+ */
31
+ observe(event, scopeFingerprint, scopeRequired = false) {
32
+ if (event.type !== 'message.received')
33
+ return;
34
+ if (event.conversation.type !== 'group')
35
+ return;
36
+ // A scope-aware channel without an active provider/application scope must
37
+ // not create an unscoped entry. Unscoped entries remain valid only for
38
+ // channels that do not declare provider scoping at all.
39
+ if (scopeRequired && scopeFingerprint === undefined)
40
+ return;
41
+ const { channel: channelId, accountId, conversation } = event;
42
+ const key = `${channelId}\u0000${accountId}`;
43
+ const chain = (this.chains.get(key) ?? Promise.resolve())
44
+ .then(() => this.record(channelId, accountId, conversation.id, conversation.externalId, conversation.name, scopeFingerprint))
45
+ .catch((error) => {
46
+ this.logger.warn(`[channel-control] conversation directory observe failed for '${channelId}:${accountId}'`, error);
47
+ })
48
+ .finally(() => {
49
+ // Drop the chain once it is the tail so the map does not grow forever.
50
+ if (this.chains.get(key) === chain)
51
+ this.chains.delete(key);
52
+ });
53
+ this.chains.set(key, chain);
54
+ }
55
+ /** All observed conversation identities for one channel+account. */
56
+ async list(channelId, accountId = 'main', scopeFingerprint, scopeRequired = false) {
57
+ const record = await this.store.get(channelId, accountId);
58
+ return Object.values(record?.conversations ?? {})
59
+ // A channel declaring an active provider scope must never resolve an
60
+ // unscoped or differently scoped historical entry. This deliberately
61
+ // makes pre-scope records re-discoverable rather than risking a cross-Bot
62
+ // group_openid reuse after an AppID change.
63
+ .filter((entry) => {
64
+ if (!scopeRequired)
65
+ return true;
66
+ if (scopeFingerprint === undefined)
67
+ return false;
68
+ return entry.scopeFingerprint === scopeFingerprint;
69
+ })
70
+ .sort((a, b) => b.lastSeenAt - a.lastSeenAt);
71
+ }
72
+ /**
73
+ * Resolve an optional platform-facing identifier to an observed canonical
74
+ * entry. QQ does not use this path: its Web UI selects discovered canonical
75
+ * group OpenIDs directly.
76
+ */
77
+ async resolveByExternalId(channelId, externalId, accountId = 'main', scopeFingerprint, scopeRequired = false) {
78
+ const entries = await this.list(channelId, accountId, scopeFingerprint, scopeRequired);
79
+ return entries.find((entry) => entry.externalId === externalId);
80
+ }
81
+ /** Read-modify-write one mapping under the channel+account scope. */
82
+ async record(channelId, accountId, canonicalId, externalId, displayName, scopeFingerprint) {
83
+ const record = (await this.store.get(channelId, accountId)) ?? {
84
+ version: 1,
85
+ conversations: {},
86
+ };
87
+ const now = this.now();
88
+ const existing = record.conversations[canonicalId];
89
+ if (!existing) {
90
+ record.conversations[canonicalId] = {
91
+ channelId,
92
+ accountId,
93
+ canonicalId,
94
+ type: 'group',
95
+ ...(externalId ? { externalId } : {}),
96
+ ...(displayName ? { displayName } : {}),
97
+ ...(scopeFingerprint ? { scopeFingerprint } : {}),
98
+ firstSeenAt: now,
99
+ lastSeenAt: now,
100
+ };
101
+ }
102
+ else {
103
+ const conflict = externalId !== undefined &&
104
+ existing.externalId !== undefined &&
105
+ existing.externalId !== externalId;
106
+ record.conversations[canonicalId] = {
107
+ ...existing,
108
+ ...(externalId ? { externalId } : {}),
109
+ ...(displayName ? { displayName } : {}),
110
+ ...(scopeFingerprint ? { scopeFingerprint } : {}),
111
+ ...(conflict ? { identityConflict: true } : {}),
112
+ lastSeenAt: now,
113
+ };
114
+ }
115
+ await this.store.set(channelId, accountId, record);
116
+ }
117
+ }
118
+ //# sourceMappingURL=conversation-directory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-directory.js","sourceRoot":"","sources":["../../src/access/conversation-directory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ChannelAccessManager — the control-plane owner of a channel's access state.
3
+ *
4
+ * Responsibilities:
5
+ * - read a channel's declared access descriptor from its definition.
6
+ * - compute access readiness from the stored policy + descriptor + owner
7
+ * discovery, including safe owner bootstrap for `ownerDiscovery='account'`
8
+ * channels (missing policy + resolvable account owner -> materialize
9
+ * and persist owner-only, then report ready).
10
+ * - validate + persist a policy via `saveAccess`.
11
+ *
12
+ * No secret ever passes through here: only canonical sender/group IDs.
13
+ */
14
+ import type { ChannelLogger, ChannelAccessPolicy } from '@krischoichoi/channel-core';
15
+ import { ChannelDefinitionRegistry } from '../definitions/registry.js';
16
+ import type { ChannelAccessState } from '../types.js';
17
+ import type { ChannelAccessPolicyStore } from './policy-store.js';
18
+ /** A store that can also expose the raw stored string (for missing-vs-invalid). */
19
+ export type ReadableChannelAccessPolicyStore = ChannelAccessPolicyStore & {
20
+ getRaw?(channelId: string, accountId: string): Promise<string | undefined>;
21
+ };
22
+ export interface ChannelAccessManagerOptions {
23
+ registry: ChannelDefinitionRegistry;
24
+ store: ReadableChannelAccessPolicyStore;
25
+ /**
26
+ * Resolve the account owner's canonical sender.id for a channel. For
27
+ * `ownerDiscovery='account'` channels this drives owner bootstrap; it is a
28
+ * no-op (returns undefined) for channels that do not implement it.
29
+ */
30
+ resolveOwnerIdentity(channelId: string, accountId: string): Promise<string | undefined>;
31
+ logger?: Pick<ChannelLogger, 'info' | 'warn'>;
32
+ }
33
+ export declare class ChannelAccessManager {
34
+ private readonly registry;
35
+ private readonly store;
36
+ private readonly resolveOwnerIdentity;
37
+ private readonly logger;
38
+ constructor(options: ChannelAccessManagerOptions);
39
+ /** Compute the access state for one channel+account. */
40
+ getState(channelId: string, accountId?: string): Promise<ChannelAccessState>;
41
+ /** Validate + persist a policy, then return the resulting access state. */
42
+ saveAccess(channelId: string, policy: ChannelAccessPolicy, accountId?: string): Promise<ChannelAccessState>;
43
+ private buildState;
44
+ /** Simple owner-source heuristic. No secrets involved. */
45
+ private deriveOwner;
46
+ }
47
+ //# sourceMappingURL=manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/access/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EACV,aAAa,EACb,mBAAmB,EACpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAEvE,OAAO,KAAK,EAGV,kBAAkB,EACnB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAIlE,mFAAmF;AACnF,MAAM,MAAM,gCAAgC,GAAG,wBAAwB,GAAG;IACxE,MAAM,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC5E,CAAC;AAEF,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,yBAAyB,CAAC;IACpC,KAAK,EAAE,gCAAgC,CAAC;IACxC;;;;OAIG;IACH,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACxF,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;CAC/C;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IACrD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmC;IACzD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAsD;IAC3F,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;gBAElD,OAAO,EAAE,2BAA2B;IAOhD,wDAAwD;IAClD,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA6ElF,2EAA2E;IACrE,UAAU,CACd,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,mBAAmB,EAC3B,SAAS,SAAS,GACjB,OAAO,CAAC,kBAAkB,CAAC;IAa9B,OAAO,CAAC,UAAU;IAclB,0DAA0D;IAC1D,OAAO,CAAC,WAAW;CAiBpB"}
@@ -0,0 +1,124 @@
1
+ import { ChannelDefinitionRegistry } from '../definitions/registry.js';
2
+ import { ControlError } from '../errors.js';
3
+ import { ownerOnlyPolicy, platformPrivatePolicy } from './materialize.js';
4
+ import { validateAccessPolicy } from './validation.js';
5
+ export class ChannelAccessManager {
6
+ registry;
7
+ store;
8
+ resolveOwnerIdentity;
9
+ logger;
10
+ constructor(options) {
11
+ this.registry = options.registry;
12
+ this.store = options.store;
13
+ this.resolveOwnerIdentity = options.resolveOwnerIdentity;
14
+ this.logger = options.logger ?? { info: () => { }, warn: () => { } };
15
+ }
16
+ /** Compute the access state for one channel+account. */
17
+ async getState(channelId, accountId = 'main') {
18
+ const definition = this.registry.require(channelId);
19
+ const descriptor = definition.access;
20
+ const stored = await this.store.get(channelId, accountId);
21
+ if (stored) {
22
+ if (descriptor.ownerDiscovery === 'platform' && !stored.ownerId) {
23
+ const ownerId = await this.resolveOwnerIdentity(channelId, accountId);
24
+ if (ownerId) {
25
+ const next = { ...stored, ownerId };
26
+ await this.store.set(channelId, accountId, next);
27
+ return this.buildState(descriptor, 'ready', next, {
28
+ configured: true,
29
+ id: ownerId,
30
+ source: 'platform',
31
+ });
32
+ }
33
+ }
34
+ if (descriptor.ownerDiscovery === 'platform' && stored.ownerId) {
35
+ const ownerId = await this.resolveOwnerIdentity(channelId, accountId);
36
+ if (ownerId === undefined) {
37
+ // A platform-scoped owner can disappear when the provider AppID is
38
+ // rotated. Do not let the previous bot's owner grant remain usable.
39
+ const { ownerId: _staleOwner, ...withoutOwner } = stored;
40
+ await this.store.set(channelId, accountId, withoutOwner);
41
+ return this.buildState(descriptor, 'ready', withoutOwner);
42
+ }
43
+ }
44
+ // A valid policy present -> ready.
45
+ return this.buildState(descriptor, 'ready', stored);
46
+ }
47
+ // `get` returned undefined: distinguish an actually-missing policy from a
48
+ // stored-but-invalid one (non-empty raw that fails to parse).
49
+ const raw = await this.store.getRaw?.(channelId, accountId);
50
+ if (raw !== undefined) {
51
+ // Stored raw exists but fails the schema -> invalid-policy (fail closed).
52
+ return this.buildState(descriptor, 'invalid-policy', undefined);
53
+ }
54
+ // No policy at all.
55
+ // ownerDiscovery='account' -> safe owner bootstrap.
56
+ if (descriptor.ownerDiscovery === 'account') {
57
+ const ownerId = await this.resolveOwnerIdentity(channelId, accountId);
58
+ if (ownerId) {
59
+ const policy = ownerOnlyPolicy(ownerId);
60
+ await this.store.set(channelId, accountId, policy);
61
+ this.logger.info(`[channel-control] access policy bootstrapped (channel=${channelId}, account=${accountId}, operation=owner-bootstrap, preset=owner-only, readiness=ready)`);
62
+ return this.buildState(descriptor, 'ready', policy, {
63
+ configured: true,
64
+ id: ownerId,
65
+ source: 'account',
66
+ });
67
+ }
68
+ // Account channel but no owner resolvable yet (e.g. no credential).
69
+ return this.buildState(descriptor, 'missing-policy', undefined);
70
+ }
71
+ // Platform-private channels guarantee that C2C messages can only originate
72
+ // from the bot creator. Materialize that narrow grant, never opening groups.
73
+ if (descriptor.ownerDiscovery === 'platform') {
74
+ const ownerId = await this.resolveOwnerIdentity(channelId, accountId);
75
+ const policy = platformPrivatePolicy(ownerId);
76
+ await this.store.set(channelId, accountId, policy);
77
+ this.logger.info(`[channel-control] access policy bootstrapped (channel=${channelId}, account=${accountId}, operation=platform-bootstrap, preset=custom, readiness=ready)`);
78
+ return this.buildState(descriptor, 'ready', policy, ownerId
79
+ ? { configured: true, id: ownerId, source: 'platform' }
80
+ : undefined);
81
+ }
82
+ // Non-account owner discovery with no policy.
83
+ if (descriptor.ownerDiscovery === 'claim') {
84
+ return this.buildState(descriptor, 'needs-owner', undefined);
85
+ }
86
+ return this.buildState(descriptor, 'missing-policy', undefined);
87
+ }
88
+ /** Validate + persist a policy, then return the resulting access state. */
89
+ async saveAccess(channelId, policy, accountId = 'main') {
90
+ const definition = this.registry.require(channelId);
91
+ const result = validateAccessPolicy(policy, definition.access);
92
+ if (!result.ok) {
93
+ this.logger.warn(`[channel-control] access policy rejected (channel=${channelId}, account=${accountId}, operation=save, reason=validation)`);
94
+ throw new ControlError('INVALID_ACCESS_POLICY', result.error);
95
+ }
96
+ await this.store.set(channelId, accountId, result.policy);
97
+ const state = await this.getState(channelId, accountId);
98
+ this.logger.info(`[channel-control] access policy saved (channel=${channelId}, account=${accountId}, operation=save, preset=${result.policy.preset}, dmPolicy=${result.policy.dmPolicy}, groupPolicy=${result.policy.groupPolicy}, readiness=${state.readiness})`);
99
+ return state;
100
+ }
101
+ buildState(descriptor, readiness, policy, ownerOverride) {
102
+ return {
103
+ descriptor,
104
+ readiness,
105
+ policy,
106
+ owner: ownerOverride ?? this.deriveOwner(policy, descriptor),
107
+ };
108
+ }
109
+ /** Simple owner-source heuristic. No secrets involved. */
110
+ deriveOwner(policy, descriptor) {
111
+ if (policy?.ownerId) {
112
+ const source = descriptor.ownerDiscovery === 'account'
113
+ ? 'account'
114
+ : descriptor.ownerDiscovery === 'claim'
115
+ ? 'claim'
116
+ : descriptor.ownerDiscovery === 'platform'
117
+ ? 'platform'
118
+ : 'manual';
119
+ return { configured: true, id: policy.ownerId, source };
120
+ }
121
+ return { configured: false };
122
+ }
123
+ }
124
+ //# sourceMappingURL=manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/access/manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Policy materialization helpers.
3
+ *
4
+ * `ownerOnlyPolicy` builds the canonical owner-only materialization:
5
+ * dmPolicy=allowlist, allowFrom=[ownerId], groupPolicy=disabled, groups={}.
6
+ *
7
+ * `rebindOwner` handles an owner change (new local operator claims/rotates):
8
+ * - owner-only: re-materialize allowFrom=[newOwner] (the old sole grant disappears).
9
+ * - allowlist/custom: update ownerId only; leave allowFrom/groups untouched so a
10
+ * re-scan / re-claim never silently rewrites a *complex* user-defined policy.
11
+ */
12
+ import type { ChannelAccessPolicy } from '@krischoichoi/channel-core';
13
+ /** Keep the persisted policy aligned with its user-facing preset. */
14
+ export declare function materializeAccessPolicy(policy: ChannelAccessPolicy): ChannelAccessPolicy;
15
+ /** The canonical owner-only policy materialization. */
16
+ export declare function ownerOnlyPolicy(ownerId: string): ChannelAccessPolicy;
17
+ /**
18
+ * Private messages are platform-restricted to the bot creator. Group access
19
+ * remains disabled until the local operator explicitly configures it.
20
+ *
21
+ * Used only when the channel declares ownerDiscovery='platform': the upstream
22
+ * platform itself restricts the private-chat audience (e.g. QQ C2C is limited
23
+ * to the bot creator). `dmPolicy: 'open'` therefore means "accept every DM
24
+ * the platform can deliver", NOT "every platform user can start a DM".
25
+ */
26
+ export declare function platformPrivatePolicy(ownerId?: string): ChannelAccessPolicy;
27
+ /**
28
+ * Rebind a policy to a new owner identity. Returns a NEW policy
29
+ * object; the input is never mutated. When `newOwner` is empty this is a no-op
30
+ * returning the input unchanged (callers guard upstream).
31
+ */
32
+ export declare function rebindOwner(policy: ChannelAccessPolicy, newOwner: string): ChannelAccessPolicy;
33
+ //# sourceMappingURL=materialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"materialize.d.ts","sourceRoot":"","sources":["../../src/access/materialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,qEAAqE;AACrE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,mBAAmB,GAAG,mBAAmB,CAUxF;AAED,uDAAuD;AACvD,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CAUpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAU3E;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,mBAAmB,EAC3B,QAAQ,EAAE,MAAM,GACf,mBAAmB,CAiBrB"}