@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.
- package/README.md +67 -0
- package/lib/access/conversation-directory-store.d.ts +73 -0
- package/lib/access/conversation-directory-store.d.ts.map +1 -0
- package/lib/access/conversation-directory-store.js +66 -0
- package/lib/access/conversation-directory-store.js.map +1 -0
- package/lib/access/conversation-directory.d.ts +51 -0
- package/lib/access/conversation-directory.d.ts.map +1 -0
- package/lib/access/conversation-directory.js +118 -0
- package/lib/access/conversation-directory.js.map +1 -0
- package/lib/access/manager.d.ts +47 -0
- package/lib/access/manager.d.ts.map +1 -0
- package/lib/access/manager.js +124 -0
- package/lib/access/manager.js.map +1 -0
- package/lib/access/materialize.d.ts +33 -0
- package/lib/access/materialize.d.ts.map +1 -0
- package/lib/access/materialize.js +67 -0
- package/lib/access/materialize.js.map +1 -0
- package/lib/access/owner-claim.d.ts +91 -0
- package/lib/access/owner-claim.d.ts.map +1 -0
- package/lib/access/owner-claim.js +200 -0
- package/lib/access/owner-claim.js.map +1 -0
- package/lib/access/policy-store.d.ts +55 -0
- package/lib/access/policy-store.d.ts.map +1 -0
- package/lib/access/policy-store.js +88 -0
- package/lib/access/policy-store.js.map +1 -0
- package/lib/access/validation.d.ts +29 -0
- package/lib/access/validation.d.ts.map +1 -0
- package/lib/access/validation.js +89 -0
- package/lib/access/validation.js.map +1 -0
- package/lib/auth/sanitizer.d.ts +35 -0
- package/lib/auth/sanitizer.d.ts.map +1 -0
- package/lib/auth/sanitizer.js +52 -0
- package/lib/auth/sanitizer.js.map +1 -0
- package/lib/auth/session-manager.d.ts +38 -0
- package/lib/auth/session-manager.d.ts.map +1 -0
- package/lib/auth/session-manager.js +219 -0
- package/lib/auth/session-manager.js.map +1 -0
- package/lib/config.d.ts +20 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +15 -0
- package/lib/config.js.map +1 -0
- package/lib/credentials/manager.d.ts +44 -0
- package/lib/credentials/manager.d.ts.map +1 -0
- package/lib/credentials/manager.js +24 -0
- package/lib/credentials/manager.js.map +1 -0
- package/lib/definitions/registry.d.ts +24 -0
- package/lib/definitions/registry.d.ts.map +1 -0
- package/lib/definitions/registry.js +59 -0
- package/lib/definitions/registry.js.map +1 -0
- package/lib/errors.d.ts +20 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +45 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +31 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +31 -0
- package/lib/index.js.map +1 -0
- package/lib/plugin.d.ts +20 -0
- package/lib/plugin.d.ts.map +1 -0
- package/lib/plugin.js +81 -0
- package/lib/plugin.js.map +1 -0
- package/lib/runtime/manager.d.ts +73 -0
- package/lib/runtime/manager.d.ts.map +1 -0
- package/lib/runtime/manager.js +277 -0
- package/lib/runtime/manager.js.map +1 -0
- package/lib/runtime/mount-handle.d.ts +11 -0
- package/lib/runtime/mount-handle.d.ts.map +1 -0
- package/lib/runtime/mount-handle.js +2 -0
- package/lib/runtime/mount-handle.js.map +1 -0
- package/lib/service.d.ts +180 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +432 -0
- package/lib/service.js.map +1 -0
- package/lib/types.d.ts +370 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/update-check.d.ts +128 -0
- package/lib/update-check.d.ts.map +1 -0
- package/lib/update-check.js +380 -0
- package/lib/update-check.js.map +1 -0
- 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":"AAqBA,mFAAmF;AACnF,MAAM,UAAU,4BAA4B,CAC1C,KAAgC;IAEhC,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9C,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,UAAU,EAAE,KAAK,CAAC,UAAU;KAC7B,CAAC;AACJ,CAAC;AASD,MAAM,OAAO,qBAAqB;IACf,KAAK,CAA6B;IAClC,GAAG,CAAe;IAClB,MAAM,CAA8B;IACrD,+DAA+D;IAC9C,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;IAE3D,YAAY,OAAqC;QAC/C,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;QACnC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;IACrD,CAAC;IAED;;;;;OAKG;IACH,OAAO,CACL,KAAmB,EACnB,gBAAyB,EACzB,aAAa,GAAG,KAAK;QAErB,IAAI,KAAK,CAAC,IAAI,KAAK,kBAAkB;YAAE,OAAO;QAC9C,IAAI,KAAK,CAAC,YAAY,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO;QAChD,0EAA0E;QAC1E,uEAAuE;QACvE,wDAAwD;QACxD,IAAI,aAAa,IAAI,gBAAgB,KAAK,SAAS;YAAE,OAAO;QAC5D,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,KAAK,CAAC;QAC9D,MAAM,GAAG,GAAG,GAAG,SAAS,SAAS,SAAS,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;aACtD,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CACrB,SAAS,EACT,SAAS,EACT,YAAY,CAAC,EAAE,EACf,YAAY,CAAC,UAAU,EACvB,YAAY,CAAC,IAAI,EACjB,gBAAgB,CACjB,CAAC;aACD,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACf,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,gEAAgE,SAAS,IAAI,SAAS,GAAG,EACzF,KAAK,CACN,CAAC;QACJ,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,uEAAuE;YACvE,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK;gBAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;QACL,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,oEAAoE;IACpE,KAAK,CAAC,IAAI,CACR,SAAiB,EACjB,SAAS,GAAG,MAAM,EAClB,gBAAyB,EACzB,aAAa,GAAG,KAAK;QAErB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC1D,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,IAAI,EAAE,CAAC;YAC/C,qEAAqE;YACrE,qEAAqE;YACrE,0EAA0E;YAC1E,4CAA4C;aAC3C,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YAChB,IAAI,CAAC,aAAa;gBAAE,OAAO,IAAI,CAAC;YAChC,IAAI,gBAAgB,KAAK,SAAS;gBAAE,OAAO,KAAK,CAAC;YACjD,OAAO,KAAK,CAAC,gBAAgB,KAAK,gBAAgB,CAAC;QACrD,CAAC,CAAC;aACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC;IACjD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,mBAAmB,CACvB,SAAiB,EACjB,UAA8B,EAC9B,SAAS,GAAG,MAAM,EAClB,gBAAyB,EACzB,aAAa,GAAG,KAAK;QAErB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAC7B,SAAS,EACT,SAAS,EACT,gBAAgB,EAChB,aAAa,CACd,CAAC;QACF,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,KAAK,UAAU,CAAC,CAAC;IAClE,CAAC;IAED,qEAAqE;IAC7D,KAAK,CAAC,MAAM,CAClB,SAAiB,EACjB,SAAiB,EACjB,WAAmB,EACnB,UAA8B,EAC9B,WAAoB,EACpB,gBAAyB;QAEzB,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,IAAI;YAC7D,OAAO,EAAE,CAAU;YACnB,aAAa,EAAE,EAAE;SAClB,CAAC;QACF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;QAEnD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,GAAG;gBAClC,SAAS;gBACT,SAAS;gBACT,WAAW;gBACX,IAAI,EAAE,OAAO;gBACb,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjD,WAAW,EAAE,GAAG;gBAChB,UAAU,EAAE,GAAG;aAChB,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,MAAM,QAAQ,GACZ,UAAU,KAAK,SAAS;gBACxB,QAAQ,CAAC,UAAU,KAAK,SAAS;gBACjC,QAAQ,CAAC,UAAU,KAAK,UAAU,CAAC;YACrC,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,GAAG;gBAClC,GAAG,QAAQ;gBACX,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjD,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/C,UAAU,EAAE,GAAG;aAChB,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IACrD,CAAC;CAEF"}
|
|
@@ -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":"AAiBA,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAO5C,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAmBvD,MAAM,OAAO,oBAAoB;IACd,QAAQ,CAA4B;IACpC,KAAK,CAAmC;IACxC,oBAAoB,CAAsD;IAC1E,MAAM,CAAuC;IAE9D,YAAY,OAAoC;QAC9C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;QACzD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,CAAC;IACrE,CAAC;IAED,wDAAwD;IACxD,KAAK,CAAC,QAAQ,CAAC,SAAiB,EAAE,SAAS,GAAG,MAAM;QAClD,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACpD,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC;QAErC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC1D,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,UAAU,CAAC,cAAc,KAAK,UAAU,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAChE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBACtE,IAAI,OAAO,EAAE,CAAC;oBACZ,MAAM,IAAI,GAAG,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,CAAC;oBACpC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;oBACjD,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE;wBAChD,UAAU,EAAE,IAAI;wBAChB,EAAE,EAAE,OAAO;wBACX,MAAM,EAAE,UAAU;qBACnB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YACD,IAAI,UAAU,CAAC,cAAc,KAAK,UAAU,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC/D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBACtE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;oBAC1B,mEAAmE;oBACnE,oEAAoE;oBACpE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,YAAY,EAAE,GAAG,MAAM,CAAC;oBACzD,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;oBACzD,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;gBAC5D,CAAC;YACH,CAAC;YACD,mCAAmC;YACnC,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACtD,CAAC;QAED,0EAA0E;QAC1E,8DAA8D;QAC9D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC5D,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,0EAA0E;YAC1E,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,gBAAgB,EAAE,SAAS,CAAC,CAAC;QAClE,CAAC;QAED,oBAAoB;QACpB,oDAAoD;QACpD,IAAI,UAAU,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;YAC5C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACtE,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;gBACxC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;gBACnD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yDAAyD,SAAS,aAAa,SAAS,kEAAkE,CAAC,CAAC;gBAC7K,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE;oBAClD,UAAU,EAAE,IAAI;oBAChB,EAAE,EAAE,OAAO;oBACX,MAAM,EAAE,SAAS;iBAClB,CAAC,CAAC;YACL,CAAC;YACD,oEAAoE;YACpE,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,gBAAgB,EAAE,SAAS,CAAC,CAAC;QAClE,CAAC;QAED,2EAA2E;QAC3E,6EAA6E;QAC7E,IAAI,UAAU,CAAC,cAAc,KAAK,UAAU,EAAE,CAAC;YAC7C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACtE,MAAM,MAAM,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;YAC9C,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;YACnD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yDAAyD,SAAS,aAAa,SAAS,iEAAiE,CAAC,CAAC;YAC5K,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO;gBACzD,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE;gBACvD,CAAC,CAAC,SAAS,CAAC,CAAC;QACjB,CAAC;QAED,8CAA8C;QAC9C,IAAI,UAAU,CAAC,cAAc,KAAK,OAAO,EAAE,CAAC;YAC1C,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,gBAAgB,EAAE,SAAS,CAAC,CAAC;IAClE,CAAC;IAED,2EAA2E;IAC3E,KAAK,CAAC,UAAU,CACd,SAAiB,EACjB,MAA2B,EAC3B,SAAS,GAAG,MAAM;QAElB,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,oBAAoB,CAAC,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,qDAAqD,SAAS,aAAa,SAAS,sCAAsC,CAAC,CAAC;YAC7I,MAAM,IAAI,YAAY,CAAC,uBAAuB,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QAChE,CAAC;QACD,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAC1D,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kDAAkD,SAAS,aAAa,SAAS,4BAA4B,MAAM,CAAC,MAAM,CAAC,MAAM,cAAc,MAAM,CAAC,MAAM,CAAC,QAAQ,iBAAiB,MAAM,CAAC,MAAM,CAAC,WAAW,eAAe,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;QACnQ,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,UAAU,CAChB,UAAmC,EACnC,SAAiC,EACjC,MAAuC,EACvC,aAA2C;QAE3C,OAAO;YACL,UAAU;YACV,SAAS;YACT,MAAM;YACN,KAAK,EAAE,aAAa,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC;SAC7D,CAAC;IACJ,CAAC;IAED,0DAA0D;IAClD,WAAW,CACjB,MAAuC,EACvC,UAAmC;QAEnC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAM,MAAM,GACV,UAAU,CAAC,cAAc,KAAK,SAAS;gBACrC,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,UAAU,CAAC,cAAc,KAAK,OAAO;oBACrC,CAAC,CAAC,OAAO;oBACT,CAAC,CAAC,UAAU,CAAC,cAAc,KAAK,UAAU;wBACxC,CAAC,CAAC,UAAU;wBACZ,CAAC,CAAC,QAAQ,CAAC;YACnB,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAC1D,CAAC;QACD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IAC/B,CAAC;CACF"}
|
|
@@ -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"}
|