@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
@@ -0,0 +1,67 @@
1
+ /** Keep the persisted policy aligned with its user-facing preset. */
2
+ export function materializeAccessPolicy(policy) {
3
+ if (policy.preset === 'owner-only') {
4
+ const { defaultGroupRule: _defaultGroupRule, ...rest } = policy;
5
+ return { ...rest, dmPolicy: 'allowlist', allowFrom: policy.ownerId ? [policy.ownerId] : [], groupPolicy: 'disabled', groups: {} };
6
+ }
7
+ if (policy.preset === 'allowlist') {
8
+ const { defaultGroupRule: _defaultGroupRule, ...rest } = policy;
9
+ return { ...rest, dmPolicy: 'allowlist', groupPolicy: 'disabled', groups: {} };
10
+ }
11
+ return policy;
12
+ }
13
+ /** The canonical owner-only policy materialization. */
14
+ export function ownerOnlyPolicy(ownerId) {
15
+ return {
16
+ version: 1,
17
+ preset: 'owner-only',
18
+ ownerId,
19
+ dmPolicy: 'allowlist',
20
+ allowFrom: [ownerId],
21
+ groupPolicy: 'disabled',
22
+ groups: {},
23
+ };
24
+ }
25
+ /**
26
+ * Private messages are platform-restricted to the bot creator. Group access
27
+ * remains disabled until the local operator explicitly configures it.
28
+ *
29
+ * Used only when the channel declares ownerDiscovery='platform': the upstream
30
+ * platform itself restricts the private-chat audience (e.g. QQ C2C is limited
31
+ * to the bot creator). `dmPolicy: 'open'` therefore means "accept every DM
32
+ * the platform can deliver", NOT "every platform user can start a DM".
33
+ */
34
+ export function platformPrivatePolicy(ownerId) {
35
+ return {
36
+ version: 1,
37
+ preset: 'custom',
38
+ dmPolicy: 'open',
39
+ allowFrom: [],
40
+ ...(ownerId ? { ownerId } : {}),
41
+ groupPolicy: 'disabled',
42
+ groups: {},
43
+ };
44
+ }
45
+ /**
46
+ * Rebind a policy to a new owner identity. Returns a NEW policy
47
+ * object; the input is never mutated. When `newOwner` is empty this is a no-op
48
+ * returning the input unchanged (callers guard upstream).
49
+ */
50
+ export function rebindOwner(policy, newOwner) {
51
+ if (!newOwner)
52
+ return policy;
53
+ if (policy.preset === 'owner-only') {
54
+ // Owner-only: the owner was the sole grantee — re-materialize allowFrom.
55
+ const { defaultGroupRule: _defaultGroupRule, ...base } = policy;
56
+ return {
57
+ ...base,
58
+ ownerId: newOwner,
59
+ allowFrom: [newOwner],
60
+ groupPolicy: 'disabled',
61
+ groups: {},
62
+ };
63
+ }
64
+ // allowlist / custom: only the ownerId changes; allowFrom/groups are preserved.
65
+ return { ...policy, ownerId: newOwner };
66
+ }
67
+ //# sourceMappingURL=materialize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"materialize.js","sourceRoot":"","sources":["../../src/access/materialize.ts"],"names":[],"mappings":"AAaA,qEAAqE;AACrE,MAAM,UAAU,uBAAuB,CAAC,MAA2B;IACjE,IAAI,MAAM,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;QACnC,MAAM,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;QAChE,OAAO,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACpI,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,MAAM,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;QAChE,OAAO,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACjF,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,OAAO;QACL,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,YAAY;QACpB,OAAO;QACP,QAAQ,EAAE,WAAW;QACrB,SAAS,EAAE,CAAC,OAAO,CAAC;QACpB,WAAW,EAAE,UAAU;QACvB,MAAM,EAAE,EAAE;KACX,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,OAAO;QACL,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,QAAQ;QAChB,QAAQ,EAAE,MAAM;QAChB,SAAS,EAAE,EAAE;QACb,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,WAAW,EAAE,UAAU;QACvB,MAAM,EAAE,EAAE;KACX,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,MAA2B,EAC3B,QAAgB;IAEhB,IAAI,CAAC,QAAQ;QAAE,OAAO,MAAM,CAAC;IAE7B,IAAI,MAAM,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;QACnC,yEAAyE;QACzE,MAAM,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;QAChE,OAAO;YACL,GAAG,IAAI;YACP,OAAO,EAAE,QAAQ;YACjB,SAAS,EAAE,CAAC,QAAQ,CAAC;YACrB,WAAW,EAAE,UAAU;YACvB,MAAM,EAAE,EAAE;SACX,CAAC;IACJ,CAAC;IAED,gFAAgF;IAChF,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * OwnerClaimSessionManager — in-memory owner-claim lifecycle.
3
+ *
4
+ * When a channel declares `ownerDiscovery='claim'`, the local operator claims
5
+ * owner identity by beginning a session, showing the short `challengeCode`,
6
+ * then sending `/dsh-claim <challengeCode>` to the bot in a DM. The manager:
7
+ * - begins a session (challenge >= 128-bit, 5 minute TTL, one active per
8
+ * channel/account),
9
+ * - observes inbound `message.received` events to capture a single valid
10
+ * candidate from a DM,
11
+ * - confirms a candidate by persisting/rebinding the access policy owner.
12
+ *
13
+ * Security rules:
14
+ * 1. Only local Web/API may begin.
15
+ * 2. 16 random bytes / >=128-bit challenge, hex-encoded (>=32 chars).
16
+ * 3. 5 minute TTL.
17
+ * 4. Single-use: one challenge maps to at most one session, and only the
18
+ * first valid candidate is captured.
19
+ * 5. Concurrent claims: one active claim per channel/account at a time —
20
+ * a repeated local begin() resumes that session rather than replacing it.
21
+ * 6. Only DM conversations are accepted; group claims are silently ignored.
22
+ * 7. A valid canonical sender.id (non-empty, !== 'unknown') is required.
23
+ * 8. Exact challenge-code match.
24
+ * 9. Candidate does not automatically become owner — local confirm does.
25
+ * 10. Local confirm writes the owner.
26
+ * 11. The claim message is consumed here and never reaches Agent/Session/
27
+ * Binding (the harness reserved gate also ensures this).
28
+ * 12. Owner changes write an audit log line.
29
+ *
30
+ * The challenge code is in-memory only, never persisted and NEVER logged.
31
+ */
32
+ import type { ChannelEvent } from '@krischoichoi/channel-core';
33
+ import { ChannelDefinitionRegistry } from '../definitions/registry.js';
34
+ import type { PublicOwnerClaimSession } from '../types.js';
35
+ import type { ChannelAccessPolicyStore } from './policy-store.js';
36
+ export declare const OWNER_CLAIM_TTL_MS: number;
37
+ export declare const OWNER_CLAIM_CHALLENGE_BYTES = 16;
38
+ /** A minimal logger-like surface (the Control logger implements this). */
39
+ export interface OwnerClaimLogger {
40
+ warn(message: string, ...args: unknown[]): void;
41
+ info(message: string, ...args: unknown[]): void;
42
+ }
43
+ export interface OwnerClaimSessionManagerOptions {
44
+ registry: ChannelDefinitionRegistry;
45
+ store: ChannelAccessPolicyStore;
46
+ logger: OwnerClaimLogger;
47
+ /** Injectable clock (ms since epoch). Defaults to Date.now. */
48
+ now?: () => number;
49
+ }
50
+ export declare class OwnerClaimSessionManager {
51
+ private readonly registry;
52
+ private readonly store;
53
+ private readonly logger;
54
+ private readonly now;
55
+ /** Active sessions keyed by `${channelId}:${accountId}`. */
56
+ private readonly sessions;
57
+ constructor(options: OwnerClaimSessionManagerOptions);
58
+ /** The (single) active claim key for a channel/account. */
59
+ private key;
60
+ private publicView;
61
+ /**
62
+ * Begin a NEW owner-claim session for a channel/account (local Web/API only).
63
+ * Rejects CLAIM_NOT_SUPPORTED when the channel does not declare
64
+ * `ownerDiscovery='claim'`. A repeated local begin for the same
65
+ * channel/account resumes the active session so a Web remount can recover
66
+ * its claim id without replacing the challenge or weakening single-use.
67
+ */
68
+ begin(channelId: string, accountId?: string): PublicOwnerClaimSession;
69
+ /** Read a session; marks + reports expiry (CLAIM_EXPIRED) when past TTL. */
70
+ get(channelId: string, claimId: string): PublicOwnerClaimSession;
71
+ /** Mark a session cancelled (idempotent). */
72
+ cancel(channelId: string, claimId: string): void;
73
+ /**
74
+ * Observe an inbound channel event. NO-OP for everything except
75
+ * `message.received`. Consumes a `/dsh-claim` command and, when every check
76
+ * passes, captures the FIRST valid candidate. NEVER throws — errors are
77
+ * caught and logged as warnings. Challenge codes are never logged.
78
+ */
79
+ observe(event: ChannelEvent): void;
80
+ /**
81
+ * Confirm a candidate, persisting/rebinding the owner, then mark
82
+ * the session confirmed. Requires phase === 'candidate' (a claim without a
83
+ * valid sender cannot be confirmed). Returns the public session.
84
+ */
85
+ confirm(channelId: string, claimId: string): Promise<PublicOwnerClaimSession>;
86
+ /** Persist/rebind the owner. */
87
+ private rebindOwner;
88
+ private findSession;
89
+ private isExpired;
90
+ }
91
+ //# sourceMappingURL=owner-claim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owner-claim.d.ts","sourceRoot":"","sources":["../../src/access/owner-claim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,4BAA4B,CAAC;AAGpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAEvE,OAAO,KAAK,EAAmB,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAGlE,eAAO,MAAM,kBAAkB,QAAa,CAAC;AAC7C,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACjD;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,EAAE,yBAAyB,CAAC;IACpC,KAAK,EAAE,wBAAwB,CAAC;IAChC,MAAM,EAAE,gBAAgB,CAAC;IACzB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAQD,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IACrD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2B;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgD;gBAE7D,OAAO,EAAE,+BAA+B;IAOpD,2DAA2D;IAC3D,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,UAAU;IAmBlB;;;;;;OAMG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,uBAAuB;IA2CrE,4EAA4E;IAC5E,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,uBAAuB;IAShE,6CAA6C;IAC7C,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAWhD;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;IAuClC;;;;OAIG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAuBnF,gCAAgC;YAClB,WAAW;IAgBzB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,SAAS;CAGlB"}
@@ -0,0 +1,200 @@
1
+ import { collectText, isReservedClaimCommand, parseOwnerClaimCommand } from '@krischoichoi/channel-core';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { ChannelDefinitionRegistry } from '../definitions/registry.js';
4
+ import { ControlError } from '../errors.js';
5
+ import { ownerOnlyPolicy, rebindOwner as rebindPolicyOwner } from './materialize.js';
6
+ export const OWNER_CLAIM_TTL_MS = 5 * 60_000;
7
+ export const OWNER_CLAIM_CHALLENGE_BYTES = 16;
8
+ export class OwnerClaimSessionManager {
9
+ registry;
10
+ store;
11
+ logger;
12
+ now;
13
+ /** Active sessions keyed by `${channelId}:${accountId}`. */
14
+ sessions = new Map();
15
+ constructor(options) {
16
+ this.registry = options.registry;
17
+ this.store = options.store;
18
+ this.logger = options.logger;
19
+ this.now = options.now ?? Date.now;
20
+ }
21
+ /** The (single) active claim key for a channel/account. */
22
+ key(channelId, accountId) {
23
+ return `${channelId}:${accountId}`;
24
+ }
25
+ publicView(session) {
26
+ // Strip the internal challenge before it can cross the API boundary. The
27
+ // public DTO carries the SAME code (the browser shows it to the operator);
28
+ // only for already-consumed/invalid phases we hide it.
29
+ const challengeCode = session.phase === 'waiting-message' || session.phase === 'candidate'
30
+ ? session.challenge
31
+ : undefined;
32
+ return {
33
+ id: session.id,
34
+ channelId: session.channelId,
35
+ accountId: session.accountId,
36
+ phase: session.phase,
37
+ ...(challengeCode !== undefined ? { challengeCode } : {}),
38
+ expiresAt: session.expiresAt,
39
+ ...(session.candidate ? { candidate: session.candidate } : {}),
40
+ };
41
+ }
42
+ /**
43
+ * Begin a NEW owner-claim session for a channel/account (local Web/API only).
44
+ * Rejects CLAIM_NOT_SUPPORTED when the channel does not declare
45
+ * `ownerDiscovery='claim'`. A repeated local begin for the same
46
+ * channel/account resumes the active session so a Web remount can recover
47
+ * its claim id without replacing the challenge or weakening single-use.
48
+ */
49
+ begin(channelId, accountId = 'main') {
50
+ const definition = this.registry.get(channelId);
51
+ if (!definition) {
52
+ throw new ControlError('CLAIM_NOT_SUPPORTED', `channel '${channelId}' is not registered`);
53
+ }
54
+ if (definition.access.ownerDiscovery !== 'claim') {
55
+ throw new ControlError('CLAIM_NOT_SUPPORTED', `channel '${channelId}' does not use owner claim discovery`);
56
+ }
57
+ const key = this.key(channelId, accountId);
58
+ const existing = this.sessions.get(key);
59
+ if (existing) {
60
+ if (this.isExpired(existing)) {
61
+ existing.phase = 'expired';
62
+ }
63
+ else if (existing.phase === 'waiting-message' || existing.phase === 'candidate') {
64
+ this.logger.info(`[channel-control] owner claim resumed (channel=${channelId}, account=${accountId}, operation=claim-resume, phase=${existing.phase}, expiresAt=${existing.expiresAt})`);
65
+ return this.publicView(existing);
66
+ }
67
+ }
68
+ const challenge = randomBytes(OWNER_CLAIM_CHALLENGE_BYTES).toString('hex');
69
+ const now = this.now();
70
+ const session = {
71
+ id: cryptoRandomId(),
72
+ channelId,
73
+ accountId,
74
+ phase: 'waiting-message',
75
+ challenge,
76
+ expiresAt: now + OWNER_CLAIM_TTL_MS,
77
+ createdAt: now,
78
+ };
79
+ this.sessions.set(key, session);
80
+ this.logger.info(`[channel-control] owner claim started (channel=${channelId}, account=${accountId}, operation=claim-begin, expiresAt=${session.expiresAt})`);
81
+ return this.publicView(session);
82
+ }
83
+ /** Read a session; marks + reports expiry (CLAIM_EXPIRED) when past TTL. */
84
+ get(channelId, claimId) {
85
+ const session = this.findSession(channelId, claimId);
86
+ if (this.isExpired(session)) {
87
+ session.phase = 'expired';
88
+ throw new ControlError('CLAIM_EXPIRED', `owner claim '${claimId}' has expired`);
89
+ }
90
+ return this.publicView(session);
91
+ }
92
+ /** Mark a session cancelled (idempotent). */
93
+ cancel(channelId, claimId) {
94
+ const session = this.findSession(channelId, claimId);
95
+ if (session.phase === 'confirmed') {
96
+ // Confirmed claims are terminal facts; report as invalid rather than
97
+ // pretending to un-confirm.
98
+ throw new ControlError('CLAIM_INVALID', 'a confirmed owner claim cannot be cancelled');
99
+ }
100
+ session.phase = 'cancelled';
101
+ this.logger.info(`[channel-control] owner claim cancelled (channel=${channelId}, account=${session.accountId}, operation=claim-cancel)`);
102
+ }
103
+ /**
104
+ * Observe an inbound channel event. NO-OP for everything except
105
+ * `message.received`. Consumes a `/dsh-claim` command and, when every check
106
+ * passes, captures the FIRST valid candidate. NEVER throws — errors are
107
+ * caught and logged as warnings. Challenge codes are never logged.
108
+ */
109
+ observe(event) {
110
+ if (event.type !== 'message.received')
111
+ return;
112
+ try {
113
+ const text = collectText(event.message.content);
114
+ if (!isReservedClaimCommand(text))
115
+ return;
116
+ const parsed = parseOwnerClaimCommand(text);
117
+ if (!parsed?.code)
118
+ return;
119
+ // Look up an ACTIVE claim for this channel/account.
120
+ const key = this.key(event.channel, event.accountId);
121
+ const session = this.sessions.get(key);
122
+ if (!session)
123
+ return; // no active local claim — silently ignore.
124
+ // The observation must not reveal a consumed/finished claim's code.
125
+ if (this.isExpired(session)) {
126
+ session.phase = 'expired';
127
+ return;
128
+ }
129
+ if (session.phase !== 'waiting-message')
130
+ return; // already has candidate / done.
131
+ // Only DM conversations may claim; group claims are silently rejected.
132
+ if (event.conversation.type !== 'dm')
133
+ return;
134
+ // Require a valid canonical sender.id.
135
+ const senderId = typeof event.sender.id === 'string' ? event.sender.id.trim() : '';
136
+ if (!senderId || senderId === 'unknown')
137
+ return;
138
+ // Exact challenge match — no partial/fuzzy reveal of the code.
139
+ if (parsed.code !== session.challenge)
140
+ return;
141
+ // First valid candidate captured; subsequent candidates ignored.
142
+ session.candidate = { senderId };
143
+ session.phase = 'candidate';
144
+ }
145
+ catch (error) {
146
+ this.logger.warn('[channel-control] owner claim observe failed', error);
147
+ }
148
+ }
149
+ /**
150
+ * Confirm a candidate, persisting/rebinding the owner, then mark
151
+ * the session confirmed. Requires phase === 'candidate' (a claim without a
152
+ * valid sender cannot be confirmed). Returns the public session.
153
+ */
154
+ async confirm(channelId, claimId) {
155
+ const session = this.findSession(channelId, claimId);
156
+ if (this.isExpired(session)) {
157
+ session.phase = 'expired';
158
+ throw new ControlError('CLAIM_EXPIRED', `owner claim '${claimId}' has expired`);
159
+ }
160
+ if (session.phase !== 'candidate' || !session.candidate) {
161
+ throw new ControlError('CLAIM_INVALID', `owner claim '${claimId}' has no candidate to confirm`);
162
+ }
163
+ const ownerId = session.candidate.senderId;
164
+ await this.rebindOwner(channelId, session.accountId, ownerId);
165
+ session.phase = 'confirmed';
166
+ this.logger.info(`[channel-control] owner claimed for '${channelId}:${session.accountId}' (claim ${claimId})`);
167
+ return this.publicView(session);
168
+ }
169
+ /** Persist/rebind the owner. */
170
+ async rebindOwner(channelId, accountId, ownerId) {
171
+ const current = await this.store.get(channelId, accountId);
172
+ let next;
173
+ if (!current) {
174
+ // No policy: materialize a fresh owner-only.
175
+ next = ownerOnlyPolicy(ownerId);
176
+ }
177
+ else {
178
+ next = rebindPolicyOwner(current, ownerId);
179
+ }
180
+ await this.store.set(channelId, accountId, next);
181
+ }
182
+ findSession(channelId, claimId) {
183
+ // A claim id is globally unique; locate it among this channel's sessions
184
+ // regardless of account.
185
+ for (const session of this.sessions.values()) {
186
+ if (session.channelId === channelId && session.id === claimId) {
187
+ return session;
188
+ }
189
+ }
190
+ throw new ControlError('CLAIM_NOT_FOUND', `owner claim '${claimId}' not found for channel '${channelId}'`);
191
+ }
192
+ isExpired(session) {
193
+ return session.phase !== 'confirmed' && session.phase !== 'cancelled' && this.now() > session.expiresAt;
194
+ }
195
+ }
196
+ /** A short, URL-safe, unguessable claim id (not the challenge). */
197
+ function cryptoRandomId() {
198
+ return randomBytes(12).toString('hex');
199
+ }
200
+ //# sourceMappingURL=owner-claim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owner-claim.js","sourceRoot":"","sources":["../../src/access/owner-claim.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Access Policy Store — durable read/write of a channel's access policy.
3
+ *
4
+ * Policies live in the shared channel-domain KV namespace (`accessPolicyStorageKey`)
5
+ * over the SAME `ChannelStorage` that adapters and the harness resolver use, so
6
+ * the writer (channel-control) and the reader (channel-harness) never hard-code
7
+ * each other's key format.
8
+ *
9
+ * The store keeps the raw/full distinction explicit:
10
+ * - `get` returns the parsed policy ONLY when it parses against the shared
11
+ * `channelAccessPolicySchema` (malformed/missing both resolve to
12
+ * `undefined`).
13
+ * - `getRaw` returns the raw JSON string so a resolver (harness-side) can
14
+ * distinguish MISSING (no raw) from INVALID (non-empty raw that
15
+ * fails to parse) — the "missing vs invalid" distinction.
16
+ */
17
+ import { type ChannelAccessPolicy, type ChannelStorage } from '@krischoichoi/channel-core';
18
+ /** Durable access-policy store abstraction used by the control plane. */
19
+ export interface ChannelAccessPolicyStore {
20
+ get(channelId: string, accountId: string): Promise<ChannelAccessPolicy | undefined>;
21
+ set(channelId: string, accountId: string, policy: ChannelAccessPolicy): Promise<void>;
22
+ delete(channelId: string, accountId: string): Promise<void>;
23
+ }
24
+ /**
25
+ * [ChannelStorageAccessPolicyStore] — a [ChannelAccessPolicyStore] backed by a
26
+ * [ChannelStorage]. Values are written as JSON strings under the shared
27
+ * versioned key. This store deliberately does NOT decide "invalid vs missing" —
28
+ * that policy lives in the harness-side resolver; here `get` simply returns the
29
+ * parsed policy when valid and `undefined` otherwise, and `getRaw` exposes the
30
+ * raw string so a caller can tell them apart.
31
+ */
32
+ export declare class ChannelStorageAccessPolicyStore implements ChannelAccessPolicyStore {
33
+ private readonly getStorage;
34
+ constructor(getStorage: () => ChannelStorage);
35
+ private key;
36
+ get(channelId: string, accountId: string): Promise<ChannelAccessPolicy | undefined>;
37
+ /** The raw JSON string under the policy key, or undefined when absent. */
38
+ getRaw(channelId: string, accountId: string): Promise<string | undefined>;
39
+ set(channelId: string, accountId: string, policy: ChannelAccessPolicy): Promise<void>;
40
+ delete(channelId: string, accountId: string): Promise<void>;
41
+ }
42
+ /**
43
+ * In-memory access-policy store (tests / transient default). Keeps raw JSON
44
+ * strings so `getRaw` can still distinguish missing vs invalid when a caller
45
+ * needs that precision; state is lost on restart.
46
+ */
47
+ export declare class MemoryAccessPolicyStore implements ChannelAccessPolicyStore {
48
+ private readonly values;
49
+ private key;
50
+ get(channelId: string, accountId: string): Promise<ChannelAccessPolicy | undefined>;
51
+ getRaw(channelId: string, accountId: string): Promise<string | undefined>;
52
+ set(channelId: string, accountId: string, policy: ChannelAccessPolicy): Promise<void>;
53
+ delete(channelId: string, accountId: string): Promise<void>;
54
+ }
55
+ //# sourceMappingURL=policy-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-store.d.ts","sourceRoot":"","sources":["../../src/access/policy-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACpB,MAAM,4BAA4B,CAAC;AAEpC,yEAAyE;AACzE,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IACpF,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtF,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D;AAaD;;;;;;;GAOG;AACH,qBAAa,+BAAgC,YAAW,wBAAwB;IAC9E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;gBAEtC,UAAU,EAAE,MAAM,cAAc;IAI5C,OAAO,CAAC,GAAG;IAIL,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAMzF,0EAA0E;IACpE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAIzE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrF,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlE;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,YAAW,wBAAwB;IACtE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IAEpD,OAAO,CAAC,GAAG;IAIL,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAMnF,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAIzE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrF,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlE"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Access Policy Store — durable read/write of a channel's access policy.
3
+ *
4
+ * Policies live in the shared channel-domain KV namespace (`accessPolicyStorageKey`)
5
+ * over the SAME `ChannelStorage` that adapters and the harness resolver use, so
6
+ * the writer (channel-control) and the reader (channel-harness) never hard-code
7
+ * each other's key format.
8
+ *
9
+ * The store keeps the raw/full distinction explicit:
10
+ * - `get` returns the parsed policy ONLY when it parses against the shared
11
+ * `channelAccessPolicySchema` (malformed/missing both resolve to
12
+ * `undefined`).
13
+ * - `getRaw` returns the raw JSON string so a resolver (harness-side) can
14
+ * distinguish MISSING (no raw) from INVALID (non-empty raw that
15
+ * fails to parse) — the "missing vs invalid" distinction.
16
+ */
17
+ import { accessPolicyStorageKey, channelAccessPolicySchema, } from '@krischoichoi/channel-core';
18
+ function parseStoredPolicy(raw) {
19
+ let value;
20
+ try {
21
+ value = JSON.parse(raw);
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ const parsed = channelAccessPolicySchema.safeParse(value);
27
+ return parsed.success ? parsed.data : undefined;
28
+ }
29
+ /**
30
+ * [ChannelStorageAccessPolicyStore] — a [ChannelAccessPolicyStore] backed by a
31
+ * [ChannelStorage]. Values are written as JSON strings under the shared
32
+ * versioned key. This store deliberately does NOT decide "invalid vs missing" —
33
+ * that policy lives in the harness-side resolver; here `get` simply returns the
34
+ * parsed policy when valid and `undefined` otherwise, and `getRaw` exposes the
35
+ * raw string so a caller can tell them apart.
36
+ */
37
+ export class ChannelStorageAccessPolicyStore {
38
+ getStorage;
39
+ constructor(getStorage) {
40
+ this.getStorage = getStorage;
41
+ }
42
+ key(channelId, accountId) {
43
+ return accessPolicyStorageKey(channelId, accountId);
44
+ }
45
+ async get(channelId, accountId) {
46
+ const raw = await this.getStorage().get(this.key(channelId, accountId));
47
+ if (raw === undefined)
48
+ return undefined;
49
+ return parseStoredPolicy(raw);
50
+ }
51
+ /** The raw JSON string under the policy key, or undefined when absent. */
52
+ async getRaw(channelId, accountId) {
53
+ return this.getStorage().get(this.key(channelId, accountId));
54
+ }
55
+ async set(channelId, accountId, policy) {
56
+ await this.getStorage().set(this.key(channelId, accountId), JSON.stringify(policy));
57
+ }
58
+ async delete(channelId, accountId) {
59
+ await this.getStorage().delete(this.key(channelId, accountId));
60
+ }
61
+ }
62
+ /**
63
+ * In-memory access-policy store (tests / transient default). Keeps raw JSON
64
+ * strings so `getRaw` can still distinguish missing vs invalid when a caller
65
+ * needs that precision; state is lost on restart.
66
+ */
67
+ export class MemoryAccessPolicyStore {
68
+ values = new Map();
69
+ key(channelId, accountId) {
70
+ return accessPolicyStorageKey(channelId, accountId);
71
+ }
72
+ async get(channelId, accountId) {
73
+ const raw = this.values.get(this.key(channelId, accountId));
74
+ if (raw === undefined)
75
+ return undefined;
76
+ return parseStoredPolicy(raw);
77
+ }
78
+ async getRaw(channelId, accountId) {
79
+ return this.values.get(this.key(channelId, accountId));
80
+ }
81
+ async set(channelId, accountId, policy) {
82
+ this.values.set(this.key(channelId, accountId), JSON.stringify(policy));
83
+ }
84
+ async delete(channelId, accountId) {
85
+ this.values.delete(this.key(channelId, accountId));
86
+ }
87
+ }
88
+ //# sourceMappingURL=policy-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-store.js","sourceRoot":"","sources":["../../src/access/policy-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Access Policy validation — the entry constrains what a policy may express
3
+ * against a channel's declared access descriptor.
4
+ *
5
+ * Validation is two-layered:
6
+ * 1. The shared `channelAccessPolicySchema` (version pinning, canonical id
7
+ * trimming, strict unknown-key rejection, type checks).
8
+ * 2. Descriptor-relative rules (owner-only needs an ownerId, mentions/groups/
9
+ * DM capability gating).
10
+ *
11
+ * Normalization performed on a validated policy: trim canonical ids and remove
12
+ * EXACT duplicates in every `allowFrom` list. Deliberately NO lowercase, no
13
+ * username resolution, no fuzzy matching — ids are opaque.
14
+ */
15
+ import { type ChannelAccessPolicy } from '@krischoichoi/channel-core';
16
+ import type { ChannelAccessDescriptor } from '../types.js';
17
+ export type AccessValidationResult = {
18
+ ok: true;
19
+ policy: ChannelAccessPolicy;
20
+ } | {
21
+ ok: false;
22
+ error: string;
23
+ };
24
+ /**
25
+ * Validate a candidate policy against a channel descriptor and, when valid,
26
+ * return a normalized copy (ids trimmed by the schema, `allowFrom` de-duplicated).
27
+ */
28
+ export declare function validateAccessPolicy(input: ChannelAccessPolicy, descriptor: ChannelAccessDescriptor): AccessValidationResult;
29
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/access/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAG3D,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,GACzC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAcjC;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,mBAAmB,EAC1B,UAAU,EAAE,uBAAuB,GAClC,sBAAsB,CAgExB"}