@intentic/sandbox-contract 1.219.0 → 1.221.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/sandbox-contract",
3
- "version": "1.219.0",
3
+ "version": "1.221.0",
4
4
  "description": "oRPC wire contract for the intentic sandbox daemon — shared by the daemon and its browser client",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -89,9 +89,9 @@
89
89
  "@orpc/contract": "1.14.13",
90
90
  "tslib": "2.8.1",
91
91
  "zod": "4.4.3",
92
- "@intentic/constants": "1.219.0",
93
- "@intentic/registry": "1.219.0",
94
- "@intentic/extension-manifest": "1.219.0"
92
+ "@intentic/constants": "1.221.0",
93
+ "@intentic/extension-manifest": "1.221.0",
94
+ "@intentic/registry": "1.221.0"
95
95
  },
96
96
  "devDependencies": {
97
97
  "@types/node": "24.13.2",
@@ -1,17 +1,18 @@
1
1
  import { oc } from "@orpc/contract";
2
- import { OkSchema, PushConfigQuerySchema, PushConfigSchema, PushEndpointSchema, PushSubscriptionSchema, PushTestSchema } from "../schemas.js";
2
+ import { OkSchema, PushChannelIdSchema, PushChannelSchema, PushConfigQuerySchema, PushConfigSchema, PushTestSchema } from "../schemas.js";
3
3
 
4
- // Web-push notifications for this sandbox. The daemon owns the VAPID keypair and the subscription list (see
4
+ // Push notifications for this sandbox. The daemon owns the VAPID keypair and the channel list (see
5
5
  // push/push-store.ts for why the key lives on the history volume), and sends on the three moments where the
6
6
  // operator's attention is genuinely wanted: a turn finished, the agent is blocked on an answer, and an
7
- // automation is waiting for approval.
7
+ // automation is waiting for approval. A channel is either a browser's web-push subscription or a native
8
+ // install reached through the platform's push relay — see PushChannelSchema for the split and why.
8
9
  //
9
10
  // `test` exists because a notification pipeline has four independent failure points the user cannot inspect
10
- // (browser permission, service-worker registration, the daemon's key, the push service itself) — a button
11
- // that proves the whole chain end-to-end is worth more than any amount of status rendering.
11
+ // (device permission, service-worker or shell registration, the daemon's key, the push service itself) — a
12
+ // button that proves the whole chain end-to-end is worth more than any amount of status rendering.
12
13
  export const pushContract = {
13
14
  config: oc.route({ method: "GET", path: "/push/config" }).input(PushConfigQuerySchema).output(PushConfigSchema),
14
- subscribe: oc.route({ method: "POST", path: "/push/subscribe" }).input(PushSubscriptionSchema).output(OkSchema),
15
- unsubscribe: oc.route({ method: "POST", path: "/push/unsubscribe" }).input(PushEndpointSchema).output(OkSchema),
15
+ subscribe: oc.route({ method: "POST", path: "/push/subscribe" }).input(PushChannelSchema).output(OkSchema),
16
+ unsubscribe: oc.route({ method: "POST", path: "/push/unsubscribe" }).input(PushChannelIdSchema).output(OkSchema),
16
17
  test: oc.route({ method: "POST", path: "/push/test" }).output(PushTestSchema),
17
18
  };
package/src/schemas.ts CHANGED
@@ -3619,9 +3619,7 @@ export const EndpointConfigSchema = z.object({
3619
3619
  * `allow`/`deny` are hostname lists (comma- or newline-separated). Empty allow = any host, each behind its
3620
3620
  * card; deny wins over allow. One capability per sandbox (singleton card): a second balance would just be a
3621
3621
  * second opinion about the same owner's wallet. */
3622
- const usdAmount = z
3623
- .string()
3624
- .regex(/^\d+(\.\d{1,6})?$/, "a USD amount like 0.50 (up to six decimals — USDC's own precision)");
3622
+ const usdAmount = z.string().regex(/^\d+(\.\d{1,6})?$/, "a USD amount like 0.50 (up to six decimals — USDC's own precision)");
3625
3623
  export const WalletNetworkSchema = z.enum(["eip155:8453", "eip155:84532"]);
3626
3624
  export type WalletNetwork = z.infer<typeof WalletNetworkSchema>;
3627
3625
  export const WalletConfigSchema = z.object({
@@ -6680,15 +6678,23 @@ export const PresenceReportSchema = z.object({
6680
6678
  });
6681
6679
  export type PresenceReport = z.infer<typeof PresenceReportSchema>;
6682
6680
 
6683
- // ---- push: web-push notifications to the owner's devices ----
6684
- // The daemon is the only tier that knows what the agent is doing, so it is the sender. Subscriptions are
6685
- // per-BROWSER (the endpoint is minted by that browser's push service Google's, Mozilla's, Apple's), which
6686
- // is why they live here and not on the platform: the platform is off the command path and would have to be
6687
- // told about every turn to be useful.
6681
+ // ---- push: notifications to the owner's devices ----
6682
+ // The daemon is the only tier that knows what the agent is doing, so it is the sender. A registration is
6683
+ // per-DEVICE and comes in two kinds, distinguished by who can be posted to directly:
6684
+ // webpush a browser (including the Android TWA, which IS Chrome). The endpoint is minted by that
6685
+ // browser's push service and the daemon sends to it directly, end-to-end encrypted.
6686
+ // relay a native app install (the iOS shell), whose OS push service (APNs) only accepts sends from
6687
+ // the app's vendor. The daemon posts plain JSON to the platform's push relay, which holds the
6688
+ // vendor credential and forwards. The payload transits the relay readable — the price of Apple
6689
+ // requiring the vendor in the loop — which is why the channel records WHERE to post (`url`)
6690
+ // rather than the daemon knowing any platform by name.
6691
+ // Channels live here and not on the platform because the daemon is on the command path: the platform would
6692
+ // have to be told about every turn to be useful.
6688
6693
 
6689
6694
  // A browser's PushSubscription, in the exact shape `web-push` consumes — the browser produces it via
6690
6695
  // PushManager.subscribe() and the client posts it back verbatim, so the daemon never reshapes it.
6691
- export const PushSubscriptionSchema = z.object({
6696
+ export const WebPushChannelSchema = z.object({
6697
+ kind: z.literal("webpush"),
6692
6698
  endpoint: z.url(),
6693
6699
  keys: z.object({
6694
6700
  // The client's public key and auth secret for payload encryption (RFC 8291). Opaque base64url here.
@@ -6696,7 +6702,27 @@ export const PushSubscriptionSchema = z.object({
6696
6702
  auth: z.string().min(1),
6697
6703
  }),
6698
6704
  });
6699
- export type PushSubscription = z.infer<typeof PushSubscriptionSchema>;
6705
+ export type WebPushChannel = z.infer<typeof WebPushChannelSchema>;
6706
+
6707
+ // A native install, addressed through a push relay. `secret` is the send capability the relay minted at
6708
+ // registration — the daemon proves it may notify this device by presenting it; the relay never learns which
6709
+ // sandbox is calling. `deviceId` doubles as the channel's identity (see channelId below).
6710
+ export const RelayChannelSchema = z.object({
6711
+ kind: z.literal("relay"),
6712
+ // The absolute URL the daemon POSTs a send to — minted by the relay at registration, stored verbatim.
6713
+ url: z.url(),
6714
+ deviceId: z.string().min(1),
6715
+ secret: z.string().min(1),
6716
+ });
6717
+ export type RelayChannel = z.infer<typeof RelayChannelSchema>;
6718
+
6719
+ export const PushChannelSchema = z.discriminatedUnion("kind", [WebPushChannelSchema, RelayChannelSchema]);
6720
+ export type PushChannel = z.infer<typeof PushChannelSchema>;
6721
+
6722
+ // The one identity every push route speaks: subscribe upserts by it, unsubscribe and the config probe name
6723
+ // devices by it. Shape-derived so the daemon and the web app can never disagree about what identifies a row —
6724
+ // a browser is its push endpoint, a native install is the deviceId its relay registration minted.
6725
+ export const channelId = (channel: PushChannel): string => (channel.kind === "webpush" ? channel.endpoint : channel.deviceId);
6700
6726
 
6701
6727
  // What the service worker renders. `url` is the in-app route the notification opens (the click handler
6702
6728
  // focuses an existing tab there rather than spawning a new one); `tag` collapses repeats — a second
@@ -6715,14 +6741,14 @@ export const PushNotificationSchema = z.object({
6715
6741
  });
6716
6742
  export type PushNotification = z.infer<typeof PushNotificationSchema>;
6717
6743
 
6718
- // The VAPID public key a browser needs to subscribe, plus whether this browser's endpoint is already known —
6719
- // so the settings toggle can render its true state instead of trusting the browser's permission alone (a
6720
- // granted permission with no server-side row would notify nothing).
6744
+ // The VAPID public key a browser needs to subscribe (native shells ignore it), plus whether the asking
6745
+ // device's channel is already known — so the settings toggle can render its true state instead of trusting
6746
+ // the device's permission alone (a granted permission with no daemon-side row would notify nothing).
6721
6747
  export const PushConfigSchema = z.object({ publicKey: z.string(), subscribed: z.boolean() });
6722
- export const PushEndpointSchema = z.object({ endpoint: z.url() });
6723
- // The optional `endpoint` says WHICH browser is asking; without it `subscribed` could only speak for the
6724
- // sandbox as a whole, which is never the question the settings toggle needs answered.
6725
- export const PushConfigQuerySchema = z.object({ endpoint: z.url().optional() });
6748
+ export const PushChannelIdSchema = z.object({ id: z.string().min(1) });
6749
+ // The optional `id` says WHICH device is asking (see channelId); without it `subscribed` could only speak
6750
+ // for the sandbox as a whole, which is never the question the settings toggle needs answered.
6751
+ export const PushConfigQuerySchema = z.object({ id: z.string().min(1).optional() });
6726
6752
 
6727
6753
  // What a test send actually achieved. `{ ok: true }` would be a lie the one place it matters most: the button
6728
6754
  // exists to prove a chain the user cannot inspect, so "the daemon accepted the request" is not the answer to