@noodleseed/one 0.187.0 → 0.188.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/commands/solutions-conversations.js +1 -1
- package/dist/commands/solutions-conversations.js.map +1 -1
- package/dist/commands/solutions-history.d.ts.map +1 -1
- package/dist/commands/solutions-history.js +5 -2
- package/dist/commands/solutions-history.js.map +1 -1
- package/dist/commands/whatsapp-ops.d.ts +1 -1
- package/dist/commands/whatsapp-ops.d.ts.map +1 -1
- package/dist/commands/whatsapp-ops.js +31 -9
- package/dist/commands/whatsapp-ops.js.map +1 -1
- package/dist/commands/whatsapp-output.d.ts +1 -1
- package/dist/commands/whatsapp-output.d.ts.map +1 -1
- package/dist/commands/whatsapp-output.js +8 -6
- package/dist/commands/whatsapp-output.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +3 -1
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-model-context.js +1 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.d.ts +89 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-coordinator.js +20 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-inbox.js +16 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-store.d.ts +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.d.ts +34 -3
- package/node_modules/@noodle-borg/assistant-gateway/dist/channel-types.js +10 -4
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +1 -1
- package/node_modules/@noodle-borg/authoring/dist/assistant.js +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-channels.js +26 -0
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-conversations.js +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-history.js +1 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +4 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +1 -1
- 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 +11 -2
- package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +1 -1
- package/node_modules/@noodle-borg/service/dist/application-history-settings.js +2 -0
- package/node_modules/@noodle-borg/service/dist/build-info.js +1 -0
- package/node_modules/@noodle-borg/service/dist/channels/composition.js +3 -2
- package/node_modules/@noodle-borg/service/dist/channels/messaging-eligibility.js +6 -1
- package/node_modules/@noodle-borg/service/dist/channels/reply-bound.js +4 -0
- package/node_modules/@noodle-borg/service/dist/channels/runtime.js +38 -10
- package/node_modules/@noodle-borg/service/dist/channels/twilio-sms.js +188 -0
- package/node_modules/@noodle-borg/service/dist/channels/twilio-webhook.js +16 -0
- package/node_modules/@noodle-borg/service/dist/conversation-history/contracts.js +1 -0
- package/node_modules/@noodle-borg/service/dist/routes/business-information-channels.js +3 -1
- package/node_modules/@noodle-borg/service/dist/routes/service-probes.js +1 -0
- package/node_modules/@noodle-borg/service/dist/routes/sms-twilio.js +66 -0
- package/node_modules/@noodle-borg/service/dist/routes/whatsapp.js +31 -21
- package/node_modules/@noodle-borg/service/dist/service.js +2 -2
- 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 +3 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/application-history-settings.d.ts +7 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/application-history-settings.js +2 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.d.ts +12 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/conversation-history.js +1 -1
- 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 +2 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/sms.d.ts +176 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/sms.js +41 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.d.ts +10 -2
- package/node_modules/@noodle-borg/wire-contracts/dist/whatsapp.js +2 -1
- package/package.json +2 -2
|
@@ -29,10 +29,12 @@ export function renderEmbeddedAssistantReference() {
|
|
|
29
29
|
'- Verify the boundary',
|
|
30
30
|
'- Troubleshooting: symptom to diagnosis',
|
|
31
31
|
'',
|
|
32
|
-
'## Public messaging: WhatsApp
|
|
32
|
+
'## Public messaging: WhatsApp, Instagram DMs and SMS',
|
|
33
33
|
'',
|
|
34
34
|
'Reuse the same server and public read-only capabilities with `publicMessaging({ channel: "whatsapp", capabilities: [product, siteIdentity] })` in `embeddedAssistant.access`. Import `publicMessaging` from `@noodleseed/one`. This projection is explicit: knowledge, pure public reads, and a `collect` opener listed with its confirmed native-record action. The platform’s natural collection gathers the opener’s fields in conversation, asks the consent question, renders the review verbatim and confirms; a field marked `private: true` never reaches the conversation model or the transcript. Other writes, connector-backed lookups, customer identity and browser widgets without an interaction are not supported. A WhatsApp participant is anonymous; never infer a customer account from its phone number. Declare Instagram direct messages the same way with `publicMessaging({ channel: "instagram", capabilities })`: at most one surface per channel, each with its own capabilities and instructions. The surface answers once an operator connects the professional account and its linked Facebook Page with `noodle channels instagram configure`, `webhook configure`, `doctor` and `enable` on a service advertising `features.instagram: 1`; replies are plain text of at most 1000 bytes, and an Instagram participant is anonymous too.',
|
|
35
35
|
'',
|
|
36
|
+
'For SMS, declare `publicMessaging({ channel: "sms", capabilities: [product, siteIdentity] })`. The first Twilio profile supports knowledge and pure public reads only; collection, writes, widgets and customer identity are unavailable. An operator uses `noodle channels sms configure`, `webhook configure`, `doctor` and `enable` with an SMS-capable Twilio number, account/number SIDs and an Auth Token secret reference on a service advertising `features.sms: 1`. Use concise plain text (aim for 300 characters; the platform bounds replies to 600). STOP/START/HELP are provider controls, never model prompts. Registration, number rental and per-segment charges remain with the Twilio account owner; technical readiness does not prove carrier delivery.',
|
|
37
|
+
'',
|
|
36
38
|
'Operator setup uses `noodle channels whatsapp configure`, managed secret references, `webhook configure`, `doctor`, and `enable`. Start paused. Only a service advertising `features.whatsapp: 1`, durable storage, a running worker, a verified cost bound and matching provider webhook can enable. Use `noodle channels whatsapp --help` for the complete operator command catalog. Keep real keys in `noodle secrets`, never in source, flags or messages. ChatGPT and Claude continue to use the same app MCP endpoint with their own host configuration.',
|
|
37
39
|
'',
|
|
38
40
|
'## Collect interactions',
|
|
@@ -107,6 +107,7 @@ function invocationContextMessages(context) {
|
|
|
107
107
|
}
|
|
108
108
|
/** What each messaging app renders; Instagram caps a reply at 1000 bytes, so the model aims well under. */
|
|
109
109
|
const MESSAGING_PROFILES = {
|
|
110
|
+
sms: 'SMS surface: concise plain text, no Markdown, at most 300 characters. Use bare URLs and answer one question at a time.',
|
|
110
111
|
whatsapp: 'WhatsApp surface: concise plain text, at most 3500 characters.',
|
|
111
112
|
instagram: 'Instagram DM surface: plain text only, no Markdown or lists, at most 600 characters, bare URLs.',
|
|
112
113
|
};
|
|
@@ -36,6 +36,27 @@ export declare class ChannelCoordinator {
|
|
|
36
36
|
actor: string;
|
|
37
37
|
readyRevision?: number | undefined;
|
|
38
38
|
readyAt?: number | undefined;
|
|
39
|
+
} | {
|
|
40
|
+
channel: "sms";
|
|
41
|
+
provider: "twilio";
|
|
42
|
+
accountSid: string;
|
|
43
|
+
phoneNumberSid: string;
|
|
44
|
+
phoneNumber: string;
|
|
45
|
+
id: string;
|
|
46
|
+
tenant: TenantRef;
|
|
47
|
+
apiKeySecret: string;
|
|
48
|
+
deploymentId: string;
|
|
49
|
+
capabilities: readonly import("./channel-types.js").ChannelCapability[];
|
|
50
|
+
supportEmail: string;
|
|
51
|
+
revision: number;
|
|
52
|
+
generation: number;
|
|
53
|
+
state: "paused" | "enabled" | "disconnected";
|
|
54
|
+
limits: import("./channel-types.js").ChannelLimits;
|
|
55
|
+
createdAt: number;
|
|
56
|
+
updatedAt: number;
|
|
57
|
+
actor: string;
|
|
58
|
+
readyRevision?: number | undefined;
|
|
59
|
+
readyAt?: number | undefined;
|
|
39
60
|
} | {
|
|
40
61
|
provider: import("./channel-types.js").ChannelProvider;
|
|
41
62
|
phoneNumberId: string;
|
|
@@ -84,6 +105,27 @@ export declare class ChannelCoordinator {
|
|
|
84
105
|
actor: string;
|
|
85
106
|
readyRevision?: number | undefined;
|
|
86
107
|
readyAt?: number | undefined;
|
|
108
|
+
} | {
|
|
109
|
+
channel: "sms";
|
|
110
|
+
provider: "twilio";
|
|
111
|
+
accountSid: string;
|
|
112
|
+
phoneNumberSid: string;
|
|
113
|
+
phoneNumber: string;
|
|
114
|
+
id: string;
|
|
115
|
+
tenant: TenantRef;
|
|
116
|
+
apiKeySecret: string;
|
|
117
|
+
deploymentId: string;
|
|
118
|
+
capabilities: readonly import("./channel-types.js").ChannelCapability[];
|
|
119
|
+
supportEmail: string;
|
|
120
|
+
revision: number;
|
|
121
|
+
generation: number;
|
|
122
|
+
state: "paused" | "enabled" | "disconnected";
|
|
123
|
+
limits: import("./channel-types.js").ChannelLimits;
|
|
124
|
+
createdAt: number;
|
|
125
|
+
updatedAt: number;
|
|
126
|
+
actor: string;
|
|
127
|
+
readyRevision?: number | undefined;
|
|
128
|
+
readyAt?: number | undefined;
|
|
87
129
|
} | {
|
|
88
130
|
provider: import("./channel-types.js").ChannelProvider;
|
|
89
131
|
phoneNumberId: string;
|
|
@@ -127,6 +169,27 @@ export declare class ChannelCoordinator {
|
|
|
127
169
|
actor: string;
|
|
128
170
|
readyRevision?: number | undefined;
|
|
129
171
|
readyAt?: number | undefined;
|
|
172
|
+
} | {
|
|
173
|
+
channel: "sms";
|
|
174
|
+
provider: "twilio";
|
|
175
|
+
accountSid: string;
|
|
176
|
+
phoneNumberSid: string;
|
|
177
|
+
phoneNumber: string;
|
|
178
|
+
id: string;
|
|
179
|
+
tenant: TenantRef;
|
|
180
|
+
apiKeySecret: string;
|
|
181
|
+
deploymentId: string;
|
|
182
|
+
capabilities: readonly import("./channel-types.js").ChannelCapability[];
|
|
183
|
+
supportEmail: string;
|
|
184
|
+
revision: number;
|
|
185
|
+
generation: number;
|
|
186
|
+
state: "paused" | "enabled" | "disconnected";
|
|
187
|
+
limits: import("./channel-types.js").ChannelLimits;
|
|
188
|
+
createdAt: number;
|
|
189
|
+
updatedAt: number;
|
|
190
|
+
actor: string;
|
|
191
|
+
readyRevision?: number | undefined;
|
|
192
|
+
readyAt?: number | undefined;
|
|
130
193
|
} | {
|
|
131
194
|
provider: import("./channel-types.js").ChannelProvider;
|
|
132
195
|
phoneNumberId: string;
|
|
@@ -151,6 +214,11 @@ export declare class ChannelCoordinator {
|
|
|
151
214
|
block(id: string, participantId: string, actor: string, key: string, until: number | null): Promise<void>;
|
|
152
215
|
unblock(id: string, participantId: string, actor: string, key: string): Promise<void>;
|
|
153
216
|
blocks(id: string, after?: string): Promise<ChannelBlock[]>;
|
|
217
|
+
optouts(id: string, after?: string): Promise<{
|
|
218
|
+
at: number;
|
|
219
|
+
blocked: boolean;
|
|
220
|
+
participantId: string;
|
|
221
|
+
}[]>;
|
|
154
222
|
events(id: string, after?: string): Promise<ChannelEvent[]>;
|
|
155
223
|
forget(id: string, participantId: string, actor: string, key: string): Promise<void>;
|
|
156
224
|
updateLimits(id: string, input: ChannelLimitsInput, actor: string, key: string, revision: number): Promise<{
|
|
@@ -173,6 +241,27 @@ export declare class ChannelCoordinator {
|
|
|
173
241
|
actor: string;
|
|
174
242
|
readyRevision?: number | undefined;
|
|
175
243
|
readyAt?: number | undefined;
|
|
244
|
+
} | {
|
|
245
|
+
channel: "sms";
|
|
246
|
+
provider: "twilio";
|
|
247
|
+
accountSid: string;
|
|
248
|
+
phoneNumberSid: string;
|
|
249
|
+
phoneNumber: string;
|
|
250
|
+
id: string;
|
|
251
|
+
tenant: TenantRef;
|
|
252
|
+
apiKeySecret: string;
|
|
253
|
+
deploymentId: string;
|
|
254
|
+
capabilities: readonly import("./channel-types.js").ChannelCapability[];
|
|
255
|
+
supportEmail: string;
|
|
256
|
+
revision: number;
|
|
257
|
+
generation: number;
|
|
258
|
+
state: "paused" | "enabled" | "disconnected";
|
|
259
|
+
limits: import("./channel-types.js").ChannelLimits;
|
|
260
|
+
createdAt: number;
|
|
261
|
+
updatedAt: number;
|
|
262
|
+
actor: string;
|
|
263
|
+
readyRevision?: number | undefined;
|
|
264
|
+
readyAt?: number | undefined;
|
|
176
265
|
} | {
|
|
177
266
|
provider: import("./channel-types.js").ChannelProvider;
|
|
178
267
|
phoneNumberId: string;
|
|
@@ -11,7 +11,8 @@ export async function requireChannel(tx, id) {
|
|
|
11
11
|
}
|
|
12
12
|
export async function channelBlocked(tx, id, participant, now) {
|
|
13
13
|
const block = await channelValue(tx, id, `block:${participant}`);
|
|
14
|
-
|
|
14
|
+
const optout = await channelValue(tx, id, `optout:${participant}`);
|
|
15
|
+
return optout?.blocked === true || (!!block && (block.until === null || block.until > now));
|
|
15
16
|
}
|
|
16
17
|
export async function requireChannelLive(tx, id) {
|
|
17
18
|
const binding = await requireChannel(tx, id);
|
|
@@ -38,6 +39,8 @@ async function mutation(tx, id, key, input, now, work) {
|
|
|
38
39
|
}
|
|
39
40
|
/** Validates a channel's provider fields and names the asset its binding will own. */
|
|
40
41
|
function configuredAsset(input) {
|
|
42
|
+
if (input.channel === 'sms')
|
|
43
|
+
return channelAssetKey('sms', input.phoneNumber);
|
|
41
44
|
if (input.channel === 'instagram')
|
|
42
45
|
return channelAssetKey('instagram', input.instagramAccountId);
|
|
43
46
|
const meta = input.provider === 'meta';
|
|
@@ -47,6 +50,8 @@ function configuredAsset(input) {
|
|
|
47
50
|
return channelAssetKey('whatsapp', input.phoneNumberId);
|
|
48
51
|
}
|
|
49
52
|
function configuredBinding(input, id, state) {
|
|
53
|
+
if (input.channel === 'sms')
|
|
54
|
+
return { ...input, id, provider: 'twilio', ...state };
|
|
50
55
|
if (input.channel === 'instagram')
|
|
51
56
|
return { ...input, id, provider: 'meta', ...state };
|
|
52
57
|
const { provider, channel: _whatsapp, ...configured } = input;
|
|
@@ -216,6 +221,20 @@ export class ChannelCoordinator {
|
|
|
216
221
|
})).map((row) => row.value);
|
|
217
222
|
});
|
|
218
223
|
}
|
|
224
|
+
async optouts(id, after) {
|
|
225
|
+
return this.store.transaction([id], async (tx) => {
|
|
226
|
+
await requireChannel(tx, id);
|
|
227
|
+
const rows = await tx.list(id, {
|
|
228
|
+
kind: 'optout',
|
|
229
|
+
limit: 100,
|
|
230
|
+
...(after ? { after: `optout:${after}` } : {}),
|
|
231
|
+
});
|
|
232
|
+
return rows.map((row) => ({
|
|
233
|
+
participantId: row.id.slice('optout:'.length),
|
|
234
|
+
...row.value,
|
|
235
|
+
}));
|
|
236
|
+
});
|
|
237
|
+
}
|
|
219
238
|
async events(id, after) {
|
|
220
239
|
return this.store.transaction([id], async (tx) => {
|
|
221
240
|
await requireChannel(tx, id);
|
|
@@ -63,7 +63,22 @@ export async function receiveChannelMessages(store, id, messages, now, retention
|
|
|
63
63
|
if ((await channelCounter(tx, id, 'ingress', now, CHANNEL_DAY_MS)) >= 10_000)
|
|
64
64
|
throw new ChannelError('ingress_capacity');
|
|
65
65
|
const participantId = channelParticipantId(binding, message.address);
|
|
66
|
-
|
|
66
|
+
let code = await refusal(tx, binding, message, participantId, pending, now);
|
|
67
|
+
if (binding.channel === 'sms' &&
|
|
68
|
+
message.control &&
|
|
69
|
+
message.eventAt >= now - CHANNEL_RETENTION_MS &&
|
|
70
|
+
message.eventAt <= now + 60_000) {
|
|
71
|
+
if (message.control !== 'help') {
|
|
72
|
+
const key = `optout:${participantId}`;
|
|
73
|
+
const prior = await channelValue(tx, id, key);
|
|
74
|
+
// Older callbacks cannot undo a newer preference; STOP wins equal-second timestamps.
|
|
75
|
+
if (!prior ||
|
|
76
|
+
message.eventAt > prior.at ||
|
|
77
|
+
(message.eventAt === prior.at && message.control === 'stop'))
|
|
78
|
+
await tx.put(id, channelRow(key, 'optout', { at: message.eventAt, blocked: message.control === 'stop' }, now));
|
|
79
|
+
}
|
|
80
|
+
code = `provider_${message.control}`;
|
|
81
|
+
}
|
|
67
82
|
const sequence = ((await channelValue(tx, id, 'sequence')) ?? 0) + 1;
|
|
68
83
|
await tx.put(id, channelRow('sequence', 'counter', sequence, now));
|
|
69
84
|
const eventId = `e_${String(sequence).padStart(20, '0')}`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Private channel journal. Each binding is a transaction/lease authority, not a browser session. */
|
|
2
2
|
export interface ChannelRow {
|
|
3
3
|
readonly id: string;
|
|
4
|
-
readonly kind: 'binding' | 'asset' | 'participant' | 'event' | 'counter' | 'spend' | 'block' | 'mutation' | 'interaction';
|
|
4
|
+
readonly kind: 'binding' | 'asset' | 'participant' | 'event' | 'counter' | 'spend' | 'block' | 'optout' | 'mutation' | 'interaction';
|
|
5
5
|
readonly state?: string;
|
|
6
6
|
readonly updatedAt: number;
|
|
7
7
|
readonly expiresAt?: number;
|
|
@@ -28,7 +28,7 @@ export type ChannelCapability = {
|
|
|
28
28
|
readonly name: string;
|
|
29
29
|
};
|
|
30
30
|
/** One binding per channel per environment; each channel owns its own provider asset. */
|
|
31
|
-
export declare const CHANNEL_KINDS: readonly ["whatsapp", "instagram"];
|
|
31
|
+
export declare const CHANNEL_KINDS: readonly ["whatsapp", "instagram", "sms"];
|
|
32
32
|
export type ChannelKind = (typeof CHANNEL_KINDS)[number];
|
|
33
33
|
export interface ChannelBindingBase {
|
|
34
34
|
readonly id: string;
|
|
@@ -68,7 +68,14 @@ export interface InstagramBinding extends ChannelBindingBase {
|
|
|
68
68
|
readonly instagramAccountId: string;
|
|
69
69
|
readonly pageId: string;
|
|
70
70
|
}
|
|
71
|
-
export
|
|
71
|
+
export interface SmsBinding extends ChannelBindingBase {
|
|
72
|
+
readonly channel: 'sms';
|
|
73
|
+
readonly provider: 'twilio';
|
|
74
|
+
readonly accountSid: string;
|
|
75
|
+
readonly phoneNumberSid: string;
|
|
76
|
+
readonly phoneNumber: string;
|
|
77
|
+
}
|
|
78
|
+
export type ChannelBinding = WhatsAppBinding | InstagramBinding | SmsBinding;
|
|
72
79
|
type ChannelConfigureBase = Pick<ChannelBindingBase, 'tenant' | 'apiKeySecret' | 'deploymentId' | 'capabilities' | 'supportEmail'> & {
|
|
73
80
|
readonly limits?: ChannelLimitsInput | undefined;
|
|
74
81
|
};
|
|
@@ -79,7 +86,8 @@ export type WhatsAppConfigure = ChannelConfigureBase & Pick<WhatsAppBinding, 'ph
|
|
|
79
86
|
readonly provider?: ChannelProvider | undefined;
|
|
80
87
|
};
|
|
81
88
|
export type InstagramConfigure = ChannelConfigureBase & Pick<InstagramBinding, 'channel' | 'instagramAccountId' | 'pageId'>;
|
|
82
|
-
export type
|
|
89
|
+
export type SmsConfigure = ChannelConfigureBase & Pick<SmsBinding, 'channel' | 'accountSid' | 'phoneNumberSid' | 'phoneNumber'>;
|
|
90
|
+
export type ChannelConfigure = WhatsAppConfigure | InstagramConfigure | SmsConfigure;
|
|
83
91
|
export interface ChannelAddress {
|
|
84
92
|
readonly kind: 'phone' | 'opaque';
|
|
85
93
|
readonly value: string;
|
|
@@ -130,6 +138,8 @@ export interface ChannelEvent {
|
|
|
130
138
|
readonly code?: string;
|
|
131
139
|
}
|
|
132
140
|
export interface ChannelInbound {
|
|
141
|
+
/** Authenticated provider opt-out control, never model input. */
|
|
142
|
+
readonly control?: 'stop' | 'start' | 'help';
|
|
133
143
|
readonly providerId: string;
|
|
134
144
|
readonly address: ChannelAddress;
|
|
135
145
|
readonly eventAt: number;
|
|
@@ -179,6 +189,27 @@ export declare function publicChannelBinding(binding: ChannelBinding): {
|
|
|
179
189
|
actor: string;
|
|
180
190
|
readyRevision?: number | undefined;
|
|
181
191
|
readyAt?: number | undefined;
|
|
192
|
+
} | {
|
|
193
|
+
channel: "sms";
|
|
194
|
+
provider: "twilio";
|
|
195
|
+
accountSid: string;
|
|
196
|
+
phoneNumberSid: string;
|
|
197
|
+
phoneNumber: string;
|
|
198
|
+
id: string;
|
|
199
|
+
tenant: TenantRef;
|
|
200
|
+
apiKeySecret: string;
|
|
201
|
+
deploymentId: string;
|
|
202
|
+
capabilities: readonly ChannelCapability[];
|
|
203
|
+
supportEmail: string;
|
|
204
|
+
revision: number;
|
|
205
|
+
generation: number;
|
|
206
|
+
state: "paused" | "enabled" | "disconnected";
|
|
207
|
+
limits: ChannelLimits;
|
|
208
|
+
createdAt: number;
|
|
209
|
+
updatedAt: number;
|
|
210
|
+
actor: string;
|
|
211
|
+
readyRevision?: number | undefined;
|
|
212
|
+
readyAt?: number | undefined;
|
|
182
213
|
} | {
|
|
183
214
|
provider: ChannelProvider;
|
|
184
215
|
phoneNumberId: string;
|
|
@@ -15,7 +15,7 @@ export const CHANNEL_DEFAULT_LIMITS = Object.freeze({
|
|
|
15
15
|
dailyMicroUsd: 20_000_000,
|
|
16
16
|
});
|
|
17
17
|
/** One binding per channel per environment; each channel owns its own provider asset. */
|
|
18
|
-
export const CHANNEL_KINDS = ['whatsapp', 'instagram'];
|
|
18
|
+
export const CHANNEL_KINDS = ['whatsapp', 'instagram', 'sms'];
|
|
19
19
|
export class ChannelError extends Error {
|
|
20
20
|
code;
|
|
21
21
|
constructor(code) {
|
|
@@ -39,7 +39,11 @@ export function channelAssetKey(channel, assetId) {
|
|
|
39
39
|
: `asset:${channel}:${channelDigest(assetId)}`;
|
|
40
40
|
}
|
|
41
41
|
export function channelBindingAsset(binding) {
|
|
42
|
-
return channelAssetKey(binding.channel, binding.channel === '
|
|
42
|
+
return channelAssetKey(binding.channel, binding.channel === 'sms'
|
|
43
|
+
? binding.phoneNumber
|
|
44
|
+
: binding.channel === 'whatsapp'
|
|
45
|
+
? binding.phoneNumberId
|
|
46
|
+
: binding.instagramAccountId);
|
|
43
47
|
}
|
|
44
48
|
export function channelParticipantId(binding, address) {
|
|
45
49
|
return `p_${createHmac('sha256', Buffer.from(binding.indexKey, 'base64'))
|
|
@@ -80,7 +84,7 @@ export async function writeChannelEvent(tx, scope, event, now) {
|
|
|
80
84
|
}
|
|
81
85
|
/** WhatsApp rows are stored without `channel`, byte-identical to rows written before Instagram. */
|
|
82
86
|
function storedChannelBinding(binding) {
|
|
83
|
-
if (binding.channel
|
|
87
|
+
if (binding.channel !== 'whatsapp')
|
|
84
88
|
return binding;
|
|
85
89
|
const { channel: _whatsapp, ...stored } = binding;
|
|
86
90
|
return stored;
|
|
@@ -91,7 +95,9 @@ export function channelBindingRow(binding, now) {
|
|
|
91
95
|
/** Rows written before Instagram carry no `channel`; they are WhatsApp. Nothing is written back. */
|
|
92
96
|
export function readChannelBinding(stored) {
|
|
93
97
|
const binding = stored;
|
|
94
|
-
return binding.channel === 'instagram'
|
|
98
|
+
return binding.channel === 'instagram' || binding.channel === 'sms'
|
|
99
|
+
? binding
|
|
100
|
+
: { ...binding, channel: 'whatsapp' };
|
|
95
101
|
}
|
|
96
102
|
export function publicChannelBinding(binding) {
|
|
97
103
|
const { indexKey: _privateIndexKey, credentialDigest: _credentialDigest, ...visible } = storedChannelBinding(binding);
|
|
@@ -224,7 +224,7 @@ export interface PublicWebsiteAccess {
|
|
|
224
224
|
}
|
|
225
225
|
export interface PublicMessagingInput {
|
|
226
226
|
/** One surface per channel; the operator binds each channel's account separately. */
|
|
227
|
-
readonly channel: 'whatsapp' | 'instagram';
|
|
227
|
+
readonly channel: 'whatsapp' | 'instagram' | 'sms';
|
|
228
228
|
readonly capabilities: readonly CapabilityRef[];
|
|
229
229
|
readonly instructions?: string;
|
|
230
230
|
/** @see AuthenticatedWebsiteInput.history */
|
|
@@ -9,7 +9,7 @@ export function publicMessaging(input) {
|
|
|
9
9
|
throw new Error(`publicMessaging does not accept ${key}`);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
if (input.channel !== 'whatsapp' && input.channel !== 'instagram')
|
|
12
|
+
if (input.channel !== 'whatsapp' && input.channel !== 'instagram' && input.channel !== 'sms')
|
|
13
13
|
throw new Error('Unsupported public messaging channel');
|
|
14
14
|
return {
|
|
15
15
|
kind: 'messaging',
|
|
@@ -86,6 +86,32 @@ export const CHANNELS_COMMAND = {
|
|
|
86
86
|
arguments: [],
|
|
87
87
|
flags: [],
|
|
88
88
|
subcommands: [
|
|
89
|
+
group('sms', 'Configure a Twilio SMS number and control two-way text messaging.', operations(leaf('configure', 'Save a paused SMS binding using a managed Twilio Auth Token reference.', [
|
|
90
|
+
revision,
|
|
91
|
+
...mutation,
|
|
92
|
+
flag('account-sid', 'Twilio Account SID (AC...).'),
|
|
93
|
+
flag('phone-number-sid', 'Twilio IncomingPhoneNumber SID (PN...).'),
|
|
94
|
+
flag('phone-number', 'Business sending number in E.164 format.'),
|
|
95
|
+
flag('api-key-secret', 'Managed secret name for the Twilio account Auth Token.'),
|
|
96
|
+
flag('capabilities', 'Comma-separated knowledge:name or tool:name selections.'),
|
|
97
|
+
flag('support-email', 'Contact shown when an answer cannot be completed.'),
|
|
98
|
+
...limits,
|
|
99
|
+
]), [
|
|
100
|
+
leaf('block', 'Block locally; Twilio opt-outs remain separately enforced.', [
|
|
101
|
+
participant,
|
|
102
|
+
blockScope,
|
|
103
|
+
flag('phone-stdin', 'Read a phone number securely from standard input.', 'boolean'),
|
|
104
|
+
flag('until', 'Expiry as an ISO timestamp.'),
|
|
105
|
+
flag('indefinite', 'Retain this block until removed.', 'boolean'),
|
|
106
|
+
...mutation,
|
|
107
|
+
]),
|
|
108
|
+
leaf('unblock', 'Remove a local block; does not override a recipient opt-out.', [
|
|
109
|
+
participant,
|
|
110
|
+
blockScope,
|
|
111
|
+
flag('phone-stdin', 'Supply the number if retained context expired.', 'boolean'),
|
|
112
|
+
...mutation,
|
|
113
|
+
]),
|
|
114
|
+
])),
|
|
89
115
|
group('whatsapp', 'Configure a 360dialog or Meta Cloud API number, test readiness, and control public messaging.', operations(leaf('configure', 'Save a paused binding using managed secret references.', [
|
|
90
116
|
revision,
|
|
91
117
|
...mutation,
|
|
@@ -31,7 +31,7 @@ const paging = (maximum) => [
|
|
|
31
31
|
type: 'string',
|
|
32
32
|
value: '<channel>',
|
|
33
33
|
summary: 'Only conversations from this channel.',
|
|
34
|
-
constraints: { choices: ['website', 'whatsapp', 'instagram'] },
|
|
34
|
+
constraints: { choices: ['website', 'whatsapp', 'instagram', 'sms'] },
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
...OPTIONAL_FLAG,
|
|
@@ -59,6 +59,7 @@ export const SOLUTION_HISTORY = {
|
|
|
59
59
|
channel('signed-in-customers', 'Record signed-in customers.'),
|
|
60
60
|
channel('whatsapp', 'Record WhatsApp conversations.'),
|
|
61
61
|
channel('instagram', 'Record Instagram DM conversations.'),
|
|
62
|
+
channel('sms', 'Record SMS conversations.'),
|
|
62
63
|
{
|
|
63
64
|
...OPTIONAL_FLAG,
|
|
64
65
|
required: true,
|
|
@@ -364,6 +364,7 @@ export declare const manifestV1Schema: z.ZodObject<{
|
|
|
364
364
|
channel: z.ZodEnum<{
|
|
365
365
|
whatsapp: "whatsapp";
|
|
366
366
|
instagram: "instagram";
|
|
367
|
+
sms: "sms";
|
|
367
368
|
}>;
|
|
368
369
|
mode: z.ZodLiteral<"public">;
|
|
369
370
|
capabilities: z.ZodArray<z.ZodObject<{
|
|
@@ -874,6 +875,7 @@ export declare const manifestV2Schema: z.ZodObject<{
|
|
|
874
875
|
channel: z.ZodEnum<{
|
|
875
876
|
whatsapp: "whatsapp";
|
|
876
877
|
instagram: "instagram";
|
|
878
|
+
sms: "sms";
|
|
877
879
|
}>;
|
|
878
880
|
mode: z.ZodLiteral<"public">;
|
|
879
881
|
capabilities: z.ZodArray<z.ZodObject<{
|
|
@@ -1679,6 +1681,7 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1679
1681
|
channel: z.ZodEnum<{
|
|
1680
1682
|
whatsapp: "whatsapp";
|
|
1681
1683
|
instagram: "instagram";
|
|
1684
|
+
sms: "sms";
|
|
1682
1685
|
}>;
|
|
1683
1686
|
mode: z.ZodLiteral<"public">;
|
|
1684
1687
|
capabilities: z.ZodArray<z.ZodObject<{
|
|
@@ -2188,6 +2191,7 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
2188
2191
|
channel: z.ZodEnum<{
|
|
2189
2192
|
whatsapp: "whatsapp";
|
|
2190
2193
|
instagram: "instagram";
|
|
2194
|
+
sms: "sms";
|
|
2191
2195
|
}>;
|
|
2192
2196
|
mode: z.ZodLiteral<"public">;
|
|
2193
2197
|
capabilities: z.ZodArray<z.ZodObject<{
|
|
@@ -199,7 +199,7 @@ const embeddedAssistantSchema = z
|
|
|
199
199
|
z
|
|
200
200
|
.object({
|
|
201
201
|
kind: z.literal('messaging'),
|
|
202
|
-
channel: z.enum(['whatsapp', 'instagram']),
|
|
202
|
+
channel: z.enum(['whatsapp', 'instagram', 'sms']),
|
|
203
203
|
mode: z.literal('public'),
|
|
204
204
|
capabilities: z.array(assistantCapabilitySchema),
|
|
205
205
|
instructions: z.string().trim().min(1).max(MAX_SERVER_INSTRUCTIONS).optional(),
|
|
@@ -19,6 +19,7 @@ import type { CompileError } from '../errors.js';
|
|
|
19
19
|
*/
|
|
20
20
|
interface AssistantSurfaceInput {
|
|
21
21
|
readonly kind?: 'website' | 'messaging' | undefined;
|
|
22
|
+
readonly channel?: string | undefined;
|
|
22
23
|
readonly mode: 'authenticated' | 'public' | 'mixed';
|
|
23
24
|
readonly capabilities?: readonly {
|
|
24
25
|
readonly kind: string;
|
|
@@ -55,7 +55,7 @@ export function validateWebsiteProjection(assistant, surfaces) {
|
|
|
55
55
|
const path = `tools.${tool.name}`;
|
|
56
56
|
if (surface.kind === 'messaging') {
|
|
57
57
|
const selected = new Set(surface.capabilities?.flatMap((entry) => (entry.kind === 'tool' ? [entry.name] : [])));
|
|
58
|
-
const requirement = messagingRequirement(tool, selected, surfaces);
|
|
58
|
+
const requirement = messagingRequirement(tool, selected, surfaces, surface.channel);
|
|
59
59
|
if (requirement !== undefined)
|
|
60
60
|
errors.push({ ...requirement, path });
|
|
61
61
|
}
|
|
@@ -87,9 +87,18 @@ export function validateWebsiteProjection(assistant, surfaces) {
|
|
|
87
87
|
* opener may carry a browser widget (the website keeps its React view) because the profile renders
|
|
88
88
|
* the same block as natural collection. Anything else names the requirement it lacks.
|
|
89
89
|
*/
|
|
90
|
-
function messagingRequirement(tool, selected, surfaces) {
|
|
90
|
+
function messagingRequirement(tool, selected, surfaces, channel) {
|
|
91
91
|
const operations = operationRefs(tool.fulfilment);
|
|
92
92
|
const interaction = surfaces.interactions?.[tool.name];
|
|
93
|
+
if (channel === 'sms' &&
|
|
94
|
+
(interaction !== undefined ||
|
|
95
|
+
operations.length > 0 ||
|
|
96
|
+
tool.annotations?.readOnlyHint !== true ||
|
|
97
|
+
tool._meta?.ui !== undefined))
|
|
98
|
+
return {
|
|
99
|
+
code: 'channel_requirement_unsupported',
|
|
100
|
+
message: `"${tool.name}" requires an interaction not supported by the SMS question-answering profile`,
|
|
101
|
+
};
|
|
93
102
|
if (interaction !== undefined && !selected.has(interaction.action)) {
|
|
94
103
|
return {
|
|
95
104
|
code: 'channel_dependency_missing',
|
|
@@ -92,7 +92,7 @@ export interface PublicAdmissionContext {
|
|
|
92
92
|
readonly messaging?: string;
|
|
93
93
|
readonly provenance?: {
|
|
94
94
|
readonly kind: 'messaging';
|
|
95
|
-
readonly channel: 'whatsapp' | 'instagram';
|
|
95
|
+
readonly channel: 'whatsapp' | 'instagram' | 'sms';
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
/** A single connector-operation invocation, fully resolved and credentialed by the runtime. */
|
|
@@ -5,6 +5,7 @@ const WIRE_SOURCES = {
|
|
|
5
5
|
signedInCustomers: 'signed_in_customers',
|
|
6
6
|
whatsapp: 'whatsapp',
|
|
7
7
|
instagram: 'instagram',
|
|
8
|
+
sms: 'sms',
|
|
8
9
|
};
|
|
9
10
|
/**
|
|
10
11
|
* The surfaces the served application declared `history: false` on (ADR 0241 decision 11), so staff
|
|
@@ -58,6 +59,7 @@ export function historySettingsProjection(setting, allowance, revision, canEdit)
|
|
|
58
59
|
signedInCustomers: setting.sources.signed_in_customers,
|
|
59
60
|
whatsapp: setting.sources.whatsapp,
|
|
60
61
|
instagram: setting.sources.instagram,
|
|
62
|
+
sms: setting.sources.sms,
|
|
61
63
|
},
|
|
62
64
|
},
|
|
63
65
|
};
|
|
@@ -7,6 +7,7 @@ export function serviceInfoPayload(buildInfo, developerMcp, channels = {}) {
|
|
|
7
7
|
features: {
|
|
8
8
|
mixedCustomerAuth: MIXED_CUSTOMER_AUTH_FEATURE_VERSION,
|
|
9
9
|
...(channels.whatsapp ? { whatsapp: 1 } : {}),
|
|
10
|
+
...(channels.sms ? { sms: 1 } : {}),
|
|
10
11
|
...(channels.instagram ? { instagram: 1 } : {}),
|
|
11
12
|
},
|
|
12
13
|
...(developerMcp
|
|
@@ -15,10 +15,11 @@ export async function createPostgresWhatsApp(pool, secretBox, options) {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
export function createWhatsAppRuntime(options, deps, history) {
|
|
18
|
-
if (!options)
|
|
18
|
+
if (!options.whatsapp)
|
|
19
19
|
return undefined;
|
|
20
20
|
return new WhatsAppRuntime({
|
|
21
|
-
...options,
|
|
21
|
+
...options.whatsapp,
|
|
22
|
+
publicBaseUrl: options.publicBaseUrl,
|
|
22
23
|
...(history.installations ? { installations: history.installations } : {}),
|
|
23
24
|
...(history.allowance
|
|
24
25
|
? {
|
|
@@ -6,7 +6,7 @@ import { isNativeRecordsOperation, } from '@noodle-borg/compiler';
|
|
|
6
6
|
* its confirmed native-record action selected beside it; everything else names, as a channel error
|
|
7
7
|
* code, the requirement the messaging profile lacks.
|
|
8
8
|
*/
|
|
9
|
-
export function messagingProjectionIneligibility(artifact) {
|
|
9
|
+
export function messagingProjectionIneligibility(artifact, channel = 'whatsapp') {
|
|
10
10
|
const interactions = artifact.toolInteractions ?? {};
|
|
11
11
|
const present = new Set(artifact.tools.map((tool) => tool.name));
|
|
12
12
|
const actions = new Set(Object.entries(interactions)
|
|
@@ -14,6 +14,11 @@ export function messagingProjectionIneligibility(artifact) {
|
|
|
14
14
|
.map(([, interaction]) => interaction.action));
|
|
15
15
|
for (const tool of artifact.tools) {
|
|
16
16
|
const interaction = interactions[tool.name];
|
|
17
|
+
if (channel === 'sms' &&
|
|
18
|
+
(interaction !== undefined ||
|
|
19
|
+
tool.annotations?.readOnlyHint !== true ||
|
|
20
|
+
tool._meta?.ui !== undefined))
|
|
21
|
+
return 'messaging_action_unsupported';
|
|
17
22
|
if (interaction !== undefined && !present.has(interaction.action))
|
|
18
23
|
return 'channel_dependency_missing';
|
|
19
24
|
const operations = operationRefs(tool.fulfilment);
|
|
@@ -3,6 +3,10 @@ const EXPAND = '\n\nAsk me to expand on any part.';
|
|
|
3
3
|
const INSTAGRAM_BYTES = 1000;
|
|
4
4
|
/** One reply within its channel's limit, cut on a character boundary and inviting a follow-up. */
|
|
5
5
|
export function boundReply(channel, answer) {
|
|
6
|
+
if (channel === 'sms')
|
|
7
|
+
return answer.length <= 600
|
|
8
|
+
? answer
|
|
9
|
+
: `${answer.slice(0, 530).replace(/[\uD800-\uDBFF]$/u, '')}${EXPAND}\nReply STOP to unsubscribe.`;
|
|
6
10
|
if (channel === 'whatsapp')
|
|
7
11
|
return answer.length <= 3500
|
|
8
12
|
? answer
|