@noodleseed/one 0.170.0 → 0.171.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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/catalog.d.ts.map +1 -1
- package/dist/commands/catalog.js +2 -1
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/whatsapp-ops.d.ts +13 -0
- package/dist/commands/whatsapp-ops.d.ts.map +1 -0
- package/dist/commands/whatsapp-ops.js +244 -0
- package/dist/commands/whatsapp-ops.js.map +1 -0
- package/dist/commands/whatsapp-output.d.ts +3 -0
- package/dist/commands/whatsapp-output.d.ts.map +1 -0
- package/dist/commands/whatsapp-output.js +70 -0
- package/dist/commands/whatsapp-output.js.map +1 -0
- package/dist/dev.js +2 -0
- package/dist/dev.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin-mode/plugin-operation-contract.d.ts +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/curated/command-groups.js +4 -0
- package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +1 -0
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +3 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +6 -0
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/app-audit/dist/audit-embedded-assistant.js +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.js +2 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-model-context.js +9 -4
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-turn-context.d.ts +16 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-turn-context.js +19 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +145 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +378 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-inbox.js +160 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-provider-blocks.d.ts +18 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-provider-blocks.js +69 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-spend.js +82 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.d.ts +42 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.js +76 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +134 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.js +72 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.d.ts +20 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-work.js +221 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.d.ts +15 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/messaging-tool-dispatch.js +34 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +19 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.js +22 -6
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +7 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +7 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-appearance-store.js +83 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-elevation.js +151 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-interaction-writes.js +45 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant-interactions.js +340 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-assistant.js +439 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-channel-store.js +102 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-continuity-store.js +159 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-elevation-store.js +110 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres-embed-store.js +134 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/postgres.js +11 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +8 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +23 -0
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +17 -2
- package/node_modules/@noodle-borg/authoring/dist/assistant.js +48 -19
- package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
- package/node_modules/@noodle-borg/authoring/dist/index.js +1 -1
- package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +121 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/index.js +1 -0
- package/node_modules/@noodle-borg/compiler/dist/artifact/config.js +1 -1
- package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +72 -12
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +51 -27
- package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.d.ts +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/website-projection.js +19 -0
- package/node_modules/@noodle-borg/module-audit/dist/index.js +37 -0
- package/node_modules/@noodle-borg/module-audit/dist/postgres-audit.js +126 -0
- package/node_modules/@noodle-borg/module-audit/dist/route.js +66 -0
- package/node_modules/@noodle-borg/runtime/dist/managed-origins.js +8 -4
- package/node_modules/@noodle-borg/runtime/dist/postgres-state-handles.js +311 -0
- package/node_modules/@noodle-borg/runtime/dist/postgres.js +4 -0
- package/node_modules/@noodle-borg/service/dist/assistant-composition.js +18 -0
- package/node_modules/@noodle-borg/service/dist/build-info.js +5 -2
- package/node_modules/@noodle-borg/service/dist/business-information/source-ingestion-coordinator.js +7 -0
- package/node_modules/@noodle-borg/service/dist/channels/360dialog.js +233 -0
- package/node_modules/@noodle-borg/service/dist/channels/cipher.js +37 -0
- package/node_modules/@noodle-borg/service/dist/channels/composition.js +37 -0
- package/node_modules/@noodle-borg/service/dist/channels/inference-guard.js +53 -0
- package/node_modules/@noodle-borg/service/dist/channels/runtime.js +337 -0
- package/node_modules/@noodle-borg/service/dist/channels/worker-loop.js +50 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +62 -34
- package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +6 -2
- package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +14 -1
- package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +238 -0
- package/node_modules/@noodle-borg/service/dist/service.js +30 -32
- package/node_modules/@noodle-borg/service/package.json +1 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.d.ts +33 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/application-channels.js +9 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/service-info.d.ts +2 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/service-info.js +10 -2
- package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +426 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +213 -0
- package/package.json +2 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChannelStore } from './channel-store.js';
|
|
2
|
+
export interface ChannelProviderBlock {
|
|
3
|
+
readonly participantId: string;
|
|
4
|
+
readonly desired: 'blocked' | 'unblocked';
|
|
5
|
+
readonly state: 'pending' | 'confirmed' | 'error' | 'unknown';
|
|
6
|
+
readonly actor: string;
|
|
7
|
+
readonly updatedAt: number;
|
|
8
|
+
readonly code?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Explicit provider operations have their own evidence; local unblock never invokes this port. */
|
|
11
|
+
export declare function beginChannelProviderBlock(store: ChannelStore, id: string, participantId: string, desired: ChannelProviderBlock['desired'], actor: string, key: string, now: number, protectedPhone?: string): Promise<{
|
|
12
|
+
address: import("./channel-types.js").ChannelAddress;
|
|
13
|
+
generation: number;
|
|
14
|
+
revision: `${string}-${string}-${string}-${string}-${string}`;
|
|
15
|
+
} | undefined>;
|
|
16
|
+
export declare function finishChannelProviderBlock(store: ChannelStore, id: string, participantId: string, revision: string, state: ChannelProviderBlock['state'], now: number, code?: string): Promise<void>;
|
|
17
|
+
export declare function channelProviderBlocks(store: ChannelStore, id: string, now: number, after?: string): Promise<ChannelProviderBlock[]>;
|
|
18
|
+
//# sourceMappingURL=channel-provider-blocks.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { requireChannel } from './channel-coordinator.js';
|
|
3
|
+
import { CHANNEL_DAY_MS, CHANNEL_RETENTION_MS, ChannelError, channelDigest, channelParticipantId, channelRow, channelValue, } from './channel-types.js';
|
|
4
|
+
/** Explicit provider operations have their own evidence; local unblock never invokes this port. */
|
|
5
|
+
export async function beginChannelProviderBlock(store, id, participantId, desired, actor, key, now, protectedPhone) {
|
|
6
|
+
return store.transaction([id], async (tx) => {
|
|
7
|
+
const binding = await requireChannel(tx, id);
|
|
8
|
+
const receipt = `provider-mutation:${channelDigest(key)}`;
|
|
9
|
+
const fingerprint = channelDigest(JSON.stringify([participantId, desired, actor]));
|
|
10
|
+
const prior = await channelValue(tx, id, receipt);
|
|
11
|
+
if (prior) {
|
|
12
|
+
if (prior.fingerprint !== fingerprint)
|
|
13
|
+
throw new ChannelError('idempotency_conflict');
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const participant = await channelValue(tx, id, participantId);
|
|
17
|
+
const address = protectedPhone
|
|
18
|
+
? { kind: 'phone', value: protectedPhone.replace(/^\+/, '') }
|
|
19
|
+
: participant?.address;
|
|
20
|
+
if (!address ||
|
|
21
|
+
address.kind !== 'phone' ||
|
|
22
|
+
channelParticipantId(binding, address) !== participantId)
|
|
23
|
+
throw new ChannelError('provider_recipient_unavailable');
|
|
24
|
+
if (desired === 'blocked' &&
|
|
25
|
+
(!participant || participant.lastInboundAt + CHANNEL_DAY_MS <= now))
|
|
26
|
+
throw new ChannelError('provider_block_window_closed');
|
|
27
|
+
const existing = await channelValue(tx, id, `provider-block:${participantId}`);
|
|
28
|
+
if (existing?.state === 'pending' && now - existing.updatedAt < 30_000)
|
|
29
|
+
throw new ChannelError('provider_operation_pending');
|
|
30
|
+
if (desired === 'unblocked' &&
|
|
31
|
+
(!existing || (existing.desired === 'unblocked' && existing.state === 'confirmed')))
|
|
32
|
+
throw new ChannelError('provider_block_not_owned');
|
|
33
|
+
const operationId = randomUUID();
|
|
34
|
+
await tx.put(id, channelRow(receipt, 'mutation', { fingerprint }, now, {
|
|
35
|
+
expiresAt: now + CHANNEL_RETENTION_MS,
|
|
36
|
+
}));
|
|
37
|
+
await tx.put(id, channelRow(`provider-block:${participantId}`, 'asset', { participantId, desired, state: 'pending', actor, updatedAt: now, operationId }, now));
|
|
38
|
+
return { address, generation: binding.generation, revision: operationId };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export async function finishChannelProviderBlock(store, id, participantId, revision, state, now, code) {
|
|
42
|
+
await store.transaction([id], async (tx) => {
|
|
43
|
+
const key = `provider-block:${participantId}`;
|
|
44
|
+
const prior = await channelValue(tx, id, key);
|
|
45
|
+
if (!prior || prior.operationId !== revision || prior.state !== 'pending')
|
|
46
|
+
throw new ChannelError('revision_conflict');
|
|
47
|
+
await tx.put(id, channelRow(key, 'asset', { ...prior, state, updatedAt: now, ...(code ? { code } : {}) }, now, prior.desired === 'unblocked' && state === 'confirmed'
|
|
48
|
+
? { expiresAt: now + CHANNEL_RETENTION_MS }
|
|
49
|
+
: {}));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export async function channelProviderBlocks(store, id, now, after) {
|
|
53
|
+
return store.transaction([id], async (tx) => {
|
|
54
|
+
await requireChannel(tx, id);
|
|
55
|
+
return (await tx.list(id, {
|
|
56
|
+
kind: 'asset',
|
|
57
|
+
limit: 100,
|
|
58
|
+
...(after ? { after: `provider-block:${after}` } : {}),
|
|
59
|
+
}))
|
|
60
|
+
.filter((row) => row.id.startsWith('provider-block:'))
|
|
61
|
+
.map((row) => {
|
|
62
|
+
const record = row.value;
|
|
63
|
+
return record.state === 'pending' && now - record.updatedAt >= 30_000
|
|
64
|
+
? { ...record, state: 'unknown', code: 'provider_outcome_unknown' }
|
|
65
|
+
: record;
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=channel-provider-blocks.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { requireChannel, requireChannelLive } from './channel-coordinator.js';
|
|
2
|
+
import { channelCounter } from './channel-inbox.js';
|
|
3
|
+
import { CHANNEL_DAY_MS, CHANNEL_RETENTION_MS, ChannelError, channelDigest, channelRow, channelValue, } from './channel-types.js';
|
|
4
|
+
const spendId = (attempt) => `spend:${channelDigest(attempt)}`;
|
|
5
|
+
const dayId = (day) => `spend-day:${day}`;
|
|
6
|
+
export async function reserveChannelSpend(store, id, attempt, maximum, now) {
|
|
7
|
+
if (!Number.isSafeInteger(maximum) || maximum <= 0)
|
|
8
|
+
throw new ChannelError('spend_bound_unverified');
|
|
9
|
+
await store.transaction([id], async (tx) => {
|
|
10
|
+
const binding = await requireChannelLive(tx, id);
|
|
11
|
+
const old = await channelValue(tx, id, spendId(attempt));
|
|
12
|
+
if (old)
|
|
13
|
+
throw new ChannelError('spend_attempt_reused');
|
|
14
|
+
const day = Math.floor(now / CHANNEL_DAY_MS);
|
|
15
|
+
const usage = (await channelValue(tx, id, dayId(day))) ?? { reserved: 0, spent: 0 };
|
|
16
|
+
if (usage.reserved + usage.spent + maximum > binding.limits.dailyMicroUsd)
|
|
17
|
+
throw new ChannelError('daily_spend_limit');
|
|
18
|
+
await tx.put(id, channelRow(spendId(attempt), 'spend', { maximum, day }, now, {
|
|
19
|
+
expiresAt: now + CHANNEL_RETENTION_MS,
|
|
20
|
+
}));
|
|
21
|
+
await tx.put(id, channelRow(dayId(day), 'spend', { ...usage, reserved: usage.reserved + maximum }, now, {
|
|
22
|
+
expiresAt: (day + 9) * CHANNEL_DAY_MS,
|
|
23
|
+
}));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export async function settleChannelSpend(store, id, attempt, actual, now) {
|
|
27
|
+
if (!Number.isSafeInteger(actual) || actual < 0)
|
|
28
|
+
throw new ChannelError('spend_usage_invalid');
|
|
29
|
+
await store.transaction([id], async (tx) => {
|
|
30
|
+
await requireChannel(tx, id);
|
|
31
|
+
const record = await channelValue(tx, id, spendId(attempt));
|
|
32
|
+
if (!record)
|
|
33
|
+
throw new ChannelError('spend_attempt_missing');
|
|
34
|
+
if (record.actual !== undefined) {
|
|
35
|
+
if (record.actual !== actual)
|
|
36
|
+
throw new ChannelError('spend_settlement_conflict');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const usage = await channelValue(tx, id, dayId(record.day));
|
|
40
|
+
if (!usage)
|
|
41
|
+
throw new ChannelError('spend_attempt_missing');
|
|
42
|
+
await tx.put(id, channelRow(spendId(attempt), 'spend', { ...record, actual }, now, {
|
|
43
|
+
expiresAt: now + CHANNEL_RETENTION_MS,
|
|
44
|
+
}));
|
|
45
|
+
await tx.put(id, channelRow(dayId(record.day), 'spend', { reserved: usage.reserved - record.maximum, spent: usage.spent + actual }, now, { expiresAt: (record.day + 9) * CHANNEL_DAY_MS }));
|
|
46
|
+
// Record actual cost even when a deployment's declared bound was violated, then stop this channel.
|
|
47
|
+
if (actual > record.maximum) {
|
|
48
|
+
const binding = await requireChannel(tx, id);
|
|
49
|
+
await tx.put(id, channelRow('binding', 'binding', {
|
|
50
|
+
...binding,
|
|
51
|
+
state: 'paused',
|
|
52
|
+
generation: binding.generation + 1,
|
|
53
|
+
revision: binding.revision + 1,
|
|
54
|
+
}, now));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export async function channelUsage(store, id, now) {
|
|
59
|
+
return store.transaction([id], async (tx) => {
|
|
60
|
+
const binding = await requireChannel(tx, id);
|
|
61
|
+
const day = Math.floor(now / CHANNEL_DAY_MS);
|
|
62
|
+
const usage = (await channelValue(tx, id, dayId(day))) ?? { reserved: 0, spent: 0 };
|
|
63
|
+
const pending = [];
|
|
64
|
+
for (const state of ['queued', 'running', 'reply', 'sending'])
|
|
65
|
+
pending.push(...(await tx.list(id, { kind: 'event', state, limit: 1000 })).map((row) => row.value));
|
|
66
|
+
return {
|
|
67
|
+
resetAt: (day + 1) * CHANNEL_DAY_MS,
|
|
68
|
+
admittedToday: await channelCounter(tx, id, 'all', now, CHANNEL_DAY_MS),
|
|
69
|
+
newParticipantsToday: await channelCounter(tx, id, 'new', now, CHANNEL_DAY_MS),
|
|
70
|
+
pending: pending.length,
|
|
71
|
+
oldestPendingAt: pending.length
|
|
72
|
+
? Math.min(...pending.map((event) => event.receivedAt))
|
|
73
|
+
: null,
|
|
74
|
+
unknownSends: await tx.count(id, 'event', 'unknown', now),
|
|
75
|
+
day: new Date(day * CHANNEL_DAY_MS).toISOString().slice(0, 10),
|
|
76
|
+
dailyMicroUsd: binding.limits.dailyMicroUsd,
|
|
77
|
+
reservedMicroUsd: usage.reserved,
|
|
78
|
+
spentMicroUsd: usage.spent,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=channel-spend.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Private channel journal. Each binding is a transaction/lease authority, not a browser session. */
|
|
2
|
+
export interface ChannelRow {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly kind: 'binding' | 'asset' | 'participant' | 'event' | 'counter' | 'spend' | 'block' | 'mutation';
|
|
5
|
+
readonly state?: string;
|
|
6
|
+
readonly updatedAt: number;
|
|
7
|
+
readonly expiresAt?: number;
|
|
8
|
+
readonly value: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface ChannelScan {
|
|
11
|
+
readonly kind: ChannelRow['kind'];
|
|
12
|
+
readonly state?: string;
|
|
13
|
+
readonly after?: string;
|
|
14
|
+
readonly limit: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ChannelTransaction {
|
|
17
|
+
get(scope: string, id: string): Promise<ChannelRow | undefined>;
|
|
18
|
+
put(scope: string, row: ChannelRow): Promise<void>;
|
|
19
|
+
remove(scope: string, id: string): Promise<void>;
|
|
20
|
+
count(scope: string, kind: ChannelRow['kind'], state: string, now: number): Promise<number>;
|
|
21
|
+
list(scope: string, scan: ChannelScan): Promise<readonly ChannelRow[]>;
|
|
22
|
+
}
|
|
23
|
+
export interface ChannelStore {
|
|
24
|
+
readonly durable: boolean;
|
|
25
|
+
transaction<T>(locks: readonly string[], work: (tx: ChannelTransaction) => Promise<T>): Promise<T>;
|
|
26
|
+
prune(now: number, limit: number): Promise<number>;
|
|
27
|
+
}
|
|
28
|
+
/** Composition supplies authenticated encryption bound to both channel and record identity. */
|
|
29
|
+
export interface ChannelCipher {
|
|
30
|
+
seal(scope: string, id: string, value: unknown): Promise<unknown>;
|
|
31
|
+
open(scope: string, id: string, sealed: unknown): Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
export declare function channelScanLimit(limit: number): number;
|
|
34
|
+
export declare function requireChannelLock(locks: readonly string[], scope: string): void;
|
|
35
|
+
/** Isolated local development adapter; never an acknowledged hosted-message authority. */
|
|
36
|
+
export declare class InMemoryChannelStore implements ChannelStore {
|
|
37
|
+
#private;
|
|
38
|
+
readonly durable = false;
|
|
39
|
+
transaction<T>(locks: readonly string[], work: (tx: ChannelTransaction) => Promise<T>): Promise<T>;
|
|
40
|
+
prune(now: number, limit: number): Promise<number>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=channel-store.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function channelScanLimit(limit) {
|
|
2
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
|
|
3
|
+
throw new Error('invalid channel scan limit');
|
|
4
|
+
return limit;
|
|
5
|
+
}
|
|
6
|
+
export function requireChannelLock(locks, scope) {
|
|
7
|
+
if (!locks.includes(scope))
|
|
8
|
+
throw new Error('channel transaction requires scope lock');
|
|
9
|
+
}
|
|
10
|
+
/** Isolated local development adapter; never an acknowledged hosted-message authority. */
|
|
11
|
+
export class InMemoryChannelStore {
|
|
12
|
+
durable = false;
|
|
13
|
+
#rows = new Map();
|
|
14
|
+
#tail = Promise.resolve();
|
|
15
|
+
async transaction(locks, work) {
|
|
16
|
+
const preceding = this.#tail;
|
|
17
|
+
let release = () => { };
|
|
18
|
+
this.#tail = new Promise((resolve) => {
|
|
19
|
+
release = resolve;
|
|
20
|
+
});
|
|
21
|
+
await preceding;
|
|
22
|
+
const rows = structuredClone(this.#rows);
|
|
23
|
+
const scoped = (scope) => {
|
|
24
|
+
requireChannelLock(locks, scope);
|
|
25
|
+
let result = rows.get(scope);
|
|
26
|
+
if (!result) {
|
|
27
|
+
result = new Map();
|
|
28
|
+
rows.set(scope, result);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
const result = await work({
|
|
34
|
+
get: async (scope, id) => structuredClone(scoped(scope).get(id)),
|
|
35
|
+
put: async (scope, row) => {
|
|
36
|
+
scoped(scope).set(row.id, structuredClone(row));
|
|
37
|
+
},
|
|
38
|
+
remove: async (scope, id) => {
|
|
39
|
+
scoped(scope).delete(id);
|
|
40
|
+
},
|
|
41
|
+
count: async (scope, kind, state, now) => [...scoped(scope).values()].filter((row) => row.kind === kind &&
|
|
42
|
+
row.state === state &&
|
|
43
|
+
(row.expiresAt === undefined || row.expiresAt > now)).length,
|
|
44
|
+
list: async (scope, scan) => [...scoped(scope).values()]
|
|
45
|
+
.filter((row) => row.kind === scan.kind &&
|
|
46
|
+
(scan.state === undefined || row.state === scan.state) &&
|
|
47
|
+
(scan.after === undefined || row.id > scan.after))
|
|
48
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
49
|
+
.slice(0, channelScanLimit(scan.limit))
|
|
50
|
+
.map((row) => structuredClone(row)),
|
|
51
|
+
});
|
|
52
|
+
this.#rows = rows;
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
release();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async prune(now, limit) {
|
|
60
|
+
channelScanLimit(limit);
|
|
61
|
+
// Share the transaction lock with writes, including scopes first created concurrently.
|
|
62
|
+
const scopes = [...this.#rows.keys()];
|
|
63
|
+
return this.transaction(scopes, async (tx) => {
|
|
64
|
+
let removed = 0;
|
|
65
|
+
for (const scope of scopes)
|
|
66
|
+
for (const row of this.#rows.get(scope)?.values() ?? []) {
|
|
67
|
+
if (removed < limit && row.expiresAt !== undefined && row.expiresAt <= now) {
|
|
68
|
+
await tx.remove(scope, row.id);
|
|
69
|
+
removed++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return removed;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=channel-store.js.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ChannelRow, ChannelTransaction } from './channel-store.js';
|
|
2
|
+
import type { TenantRef } from './tenant-ref.js';
|
|
3
|
+
export declare const CHANNEL_DAY_MS = 86400000;
|
|
4
|
+
export declare const CHANNEL_RETENTION_MS: number;
|
|
5
|
+
export declare const CHANNEL_REGISTRY = "assistant-channels";
|
|
6
|
+
export declare const CHANNEL_DEFAULT_LIMITS: Readonly<{
|
|
7
|
+
perMinute: 10;
|
|
8
|
+
perHour: 60;
|
|
9
|
+
perDay: 200;
|
|
10
|
+
channelPerDay: 1000;
|
|
11
|
+
newParticipantsPerDay: 200;
|
|
12
|
+
concurrent: 5;
|
|
13
|
+
pendingPerParticipant: 3;
|
|
14
|
+
pending: 100;
|
|
15
|
+
textCharacters: 4000;
|
|
16
|
+
dailyMicroUsd: 20000000;
|
|
17
|
+
}>;
|
|
18
|
+
export type ChannelLimits = {
|
|
19
|
+
-readonly [K in keyof typeof CHANNEL_DEFAULT_LIMITS]: number;
|
|
20
|
+
};
|
|
21
|
+
export type ChannelLimitsInput = {
|
|
22
|
+
[K in keyof ChannelLimits]?: number | undefined;
|
|
23
|
+
};
|
|
24
|
+
export type ChannelCapability = {
|
|
25
|
+
readonly kind: 'tool' | 'knowledge';
|
|
26
|
+
readonly name: string;
|
|
27
|
+
};
|
|
28
|
+
export interface ChannelBinding {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly tenant: TenantRef;
|
|
31
|
+
readonly provider: '360dialog';
|
|
32
|
+
readonly phoneNumberId: string;
|
|
33
|
+
readonly apiKeySecret: string;
|
|
34
|
+
readonly webhookSecret: string;
|
|
35
|
+
readonly deploymentId: string;
|
|
36
|
+
readonly capabilities: readonly ChannelCapability[];
|
|
37
|
+
readonly supportEmail: string;
|
|
38
|
+
readonly revision: number;
|
|
39
|
+
readonly generation: number;
|
|
40
|
+
readonly state: 'paused' | 'enabled' | 'disconnected';
|
|
41
|
+
/** Private, encrypted indexing seed. Never projected to an operator response or model. */
|
|
42
|
+
readonly indexKey: string;
|
|
43
|
+
readonly credentialDigest?: string;
|
|
44
|
+
readonly limits: ChannelLimits;
|
|
45
|
+
readonly createdAt: number;
|
|
46
|
+
readonly updatedAt: number;
|
|
47
|
+
readonly actor: string;
|
|
48
|
+
readonly readyRevision?: number | undefined;
|
|
49
|
+
readonly readyAt?: number | undefined;
|
|
50
|
+
}
|
|
51
|
+
export type ChannelConfigure = Pick<ChannelBinding, 'tenant' | 'phoneNumberId' | 'apiKeySecret' | 'webhookSecret' | 'deploymentId' | 'capabilities' | 'supportEmail'> & {
|
|
52
|
+
readonly limits?: ChannelLimitsInput | undefined;
|
|
53
|
+
};
|
|
54
|
+
export interface ChannelAddress {
|
|
55
|
+
readonly kind: 'phone' | 'opaque';
|
|
56
|
+
readonly value: string;
|
|
57
|
+
}
|
|
58
|
+
export interface ChannelHistory {
|
|
59
|
+
readonly role: 'user' | 'assistant';
|
|
60
|
+
readonly content: string;
|
|
61
|
+
readonly at: number;
|
|
62
|
+
}
|
|
63
|
+
export interface ChannelParticipant {
|
|
64
|
+
readonly id: string;
|
|
65
|
+
readonly address: ChannelAddress;
|
|
66
|
+
readonly lastInboundAt: number;
|
|
67
|
+
readonly history: readonly ChannelHistory[];
|
|
68
|
+
readonly strikes: readonly number[];
|
|
69
|
+
readonly deploymentId?: string;
|
|
70
|
+
readonly modelToolUses?: readonly string[];
|
|
71
|
+
readonly cooldownUntil?: number;
|
|
72
|
+
}
|
|
73
|
+
export type ChannelEventState = 'queued' | 'running' | 'reply' | 'sending' | 'accepted' | 'delivered' | 'read' | 'refused' | 'expired' | 'failed' | 'cancelled' | 'unknown';
|
|
74
|
+
export interface ChannelEvent {
|
|
75
|
+
readonly id: string;
|
|
76
|
+
readonly participantId: string;
|
|
77
|
+
readonly providerId: string;
|
|
78
|
+
readonly providerMessageId?: string;
|
|
79
|
+
readonly text?: string;
|
|
80
|
+
readonly reply?: string;
|
|
81
|
+
readonly eventAt: number;
|
|
82
|
+
readonly receivedAt: number;
|
|
83
|
+
readonly generation: number;
|
|
84
|
+
readonly deploymentId: string;
|
|
85
|
+
readonly state: ChannelEventState;
|
|
86
|
+
readonly attempts: number;
|
|
87
|
+
readonly lease?: string | undefined;
|
|
88
|
+
readonly leaseUntil?: number | undefined;
|
|
89
|
+
readonly turnDeadline?: number;
|
|
90
|
+
readonly code?: string;
|
|
91
|
+
}
|
|
92
|
+
export interface ChannelInbound {
|
|
93
|
+
readonly providerId: string;
|
|
94
|
+
readonly address: ChannelAddress;
|
|
95
|
+
readonly eventAt: number;
|
|
96
|
+
readonly text?: string;
|
|
97
|
+
}
|
|
98
|
+
export declare class ChannelError extends Error {
|
|
99
|
+
readonly code: string;
|
|
100
|
+
constructor(code: string);
|
|
101
|
+
}
|
|
102
|
+
export declare function channelDigest(value: string): string;
|
|
103
|
+
export declare function channelTenantKey(tenant: TenantRef): string;
|
|
104
|
+
export declare function channelParticipantId(binding: ChannelBinding, address: ChannelAddress): string;
|
|
105
|
+
export declare function sameChannelTenant(left: TenantRef, right: TenantRef): boolean;
|
|
106
|
+
export declare function channelLimits(input?: ChannelLimitsInput): ChannelLimits;
|
|
107
|
+
/** Row values are private, encrypted implementation records, never unchecked API payloads. */
|
|
108
|
+
export declare function channelValue<T>(tx: ChannelTransaction, scope: string, id: string): Promise<T | undefined>;
|
|
109
|
+
export declare function channelRow(id: string, kind: ChannelRow['kind'], value: unknown, now: number, options?: {
|
|
110
|
+
state?: string;
|
|
111
|
+
expiresAt?: number;
|
|
112
|
+
}): ChannelRow;
|
|
113
|
+
export declare function writeChannelEvent(tx: ChannelTransaction, scope: string, event: ChannelEvent, now: number): Promise<void>;
|
|
114
|
+
export declare function publicChannelBinding(binding: ChannelBinding): {
|
|
115
|
+
id: string;
|
|
116
|
+
tenant: TenantRef;
|
|
117
|
+
provider: "360dialog";
|
|
118
|
+
phoneNumberId: string;
|
|
119
|
+
apiKeySecret: string;
|
|
120
|
+
webhookSecret: string;
|
|
121
|
+
deploymentId: string;
|
|
122
|
+
capabilities: readonly ChannelCapability[];
|
|
123
|
+
supportEmail: string;
|
|
124
|
+
revision: number;
|
|
125
|
+
generation: number;
|
|
126
|
+
state: "paused" | "enabled" | "disconnected";
|
|
127
|
+
limits: ChannelLimits;
|
|
128
|
+
createdAt: number;
|
|
129
|
+
updatedAt: number;
|
|
130
|
+
actor: string;
|
|
131
|
+
readyRevision?: number | undefined;
|
|
132
|
+
readyAt?: number | undefined;
|
|
133
|
+
};
|
|
134
|
+
//# sourceMappingURL=channel-types.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
2
|
+
export const CHANNEL_DAY_MS = 86_400_000;
|
|
3
|
+
export const CHANNEL_RETENTION_MS = 7 * CHANNEL_DAY_MS;
|
|
4
|
+
export const CHANNEL_REGISTRY = 'assistant-channels';
|
|
5
|
+
export const CHANNEL_DEFAULT_LIMITS = Object.freeze({
|
|
6
|
+
perMinute: 10,
|
|
7
|
+
perHour: 60,
|
|
8
|
+
perDay: 200,
|
|
9
|
+
channelPerDay: 1000,
|
|
10
|
+
newParticipantsPerDay: 200,
|
|
11
|
+
concurrent: 5,
|
|
12
|
+
pendingPerParticipant: 3,
|
|
13
|
+
pending: 100,
|
|
14
|
+
textCharacters: 4000,
|
|
15
|
+
dailyMicroUsd: 20_000_000,
|
|
16
|
+
});
|
|
17
|
+
export class ChannelError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
constructor(code) {
|
|
20
|
+
super(code);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.name = 'ChannelError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function channelDigest(value) {
|
|
26
|
+
return createHash('sha256').update(value).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
export function channelTenantKey(tenant) {
|
|
29
|
+
return `tenant:${channelDigest(JSON.stringify([tenant.org, tenant.app, tenant.env]))}`;
|
|
30
|
+
}
|
|
31
|
+
export function channelParticipantId(binding, address) {
|
|
32
|
+
return `p_${createHmac('sha256', Buffer.from(binding.indexKey, 'base64'))
|
|
33
|
+
.update(JSON.stringify([binding.id, address.kind, address.value]))
|
|
34
|
+
.digest('hex')}`;
|
|
35
|
+
}
|
|
36
|
+
export function sameChannelTenant(left, right) {
|
|
37
|
+
return left.org === right.org && left.app === right.app && left.env === right.env;
|
|
38
|
+
}
|
|
39
|
+
export function channelLimits(input = {}) {
|
|
40
|
+
const limits = { ...CHANNEL_DEFAULT_LIMITS };
|
|
41
|
+
for (const key of Object.keys(input)) {
|
|
42
|
+
if (!(key in limits))
|
|
43
|
+
throw new ChannelError('limits_invalid');
|
|
44
|
+
const name = key;
|
|
45
|
+
const value = input[name];
|
|
46
|
+
if (value === undefined ||
|
|
47
|
+
!Number.isSafeInteger(value) ||
|
|
48
|
+
value < 0 ||
|
|
49
|
+
value > CHANNEL_DEFAULT_LIMITS[name])
|
|
50
|
+
throw new ChannelError('limits_invalid');
|
|
51
|
+
limits[name] = value;
|
|
52
|
+
}
|
|
53
|
+
return limits;
|
|
54
|
+
}
|
|
55
|
+
/** Row values are private, encrypted implementation records, never unchecked API payloads. */
|
|
56
|
+
export async function channelValue(tx, scope, id) {
|
|
57
|
+
return (await tx.get(scope, id))?.value;
|
|
58
|
+
}
|
|
59
|
+
export function channelRow(id, kind, value, now, options = {}) {
|
|
60
|
+
return { id, kind, value, updatedAt: now, ...options };
|
|
61
|
+
}
|
|
62
|
+
export async function writeChannelEvent(tx, scope, event, now) {
|
|
63
|
+
await tx.put(scope, channelRow(event.id, 'event', event, now, {
|
|
64
|
+
state: event.state,
|
|
65
|
+
expiresAt: event.receivedAt + CHANNEL_RETENTION_MS,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
export function publicChannelBinding(binding) {
|
|
69
|
+
const { indexKey: _privateIndexKey, credentialDigest: _credentialDigest, ...visible } = binding;
|
|
70
|
+
return visible;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=channel-types.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ChannelStore } from './channel-store.js';
|
|
2
|
+
import { type ChannelBinding, type ChannelEvent, type ChannelParticipant } from './channel-types.js';
|
|
3
|
+
export interface ChannelWork {
|
|
4
|
+
readonly binding: ChannelBinding;
|
|
5
|
+
readonly participant: ChannelParticipant;
|
|
6
|
+
readonly event: ChannelEvent;
|
|
7
|
+
}
|
|
8
|
+
export declare function claimChannelTurn(store: ChannelStore, id: string, now: number, retentionMs?: number): Promise<ChannelWork | undefined>;
|
|
9
|
+
export declare function renewChannelTurn(store: ChannelStore, id: string, eventId: string, lease: string, now: number): Promise<void>;
|
|
10
|
+
export declare function completeChannelTurn(store: ChannelStore, id: string, eventId: string, lease: string, reply: string, now: number, retentionMs?: number, code?: string): Promise<void>;
|
|
11
|
+
export declare function prepareChannelSend(store: ChannelStore, id: string, now: number): Promise<ChannelWork | undefined>;
|
|
12
|
+
export declare function updateChannelSend(store: ChannelStore, id: string, eventId: string, lease: string, result: {
|
|
13
|
+
state: 'accepted' | 'unknown' | 'failed';
|
|
14
|
+
providerMessageId?: string;
|
|
15
|
+
code?: string;
|
|
16
|
+
}, now: number): Promise<void>;
|
|
17
|
+
export declare function recordChannelDelivery(store: ChannelStore, id: string, providerMessageId: string, state: 'sent' | 'delivered' | 'read' | 'failed', now: number): Promise<void>;
|
|
18
|
+
/** Last local fence before the adapter starts external IO; already dispatched work cannot be recalled. */
|
|
19
|
+
export declare function assertChannelSend(store: ChannelStore, id: string, eventId: string, lease: string, now: number): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=channel-work.d.ts.map
|