@immediately-run/sdk 0.2.8 → 0.3.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/auth.cjs CHANGED
@@ -23,20 +23,20 @@ __export(auth_exports, {
23
23
  useAuth: () => useAuth
24
24
  });
25
25
  module.exports = __toCommonJS(auth_exports);
26
- var import_react = require("react");
27
- const authService = () => {
28
- return module.evaluation.module.bundler.auth;
29
- };
30
- const getAuthState = () => authService().getState();
31
- const onAuthChange = (listener) => {
32
- const disposable = authService().onChange(listener);
33
- return () => disposable.dispose();
34
- };
35
- const useAuth = () => {
36
- const [state, setState] = (0, import_react.useState)(getAuthState);
37
- (0, import_react.useEffect)(() => onAuthChange(setState), []);
38
- return state;
26
+ var import_pushChannel = require("./pushChannel");
27
+ const isAuthState = (v) => {
28
+ const s = v;
29
+ return !!s && (s.status === "unknown" || s.status === "signed-in" || s.status === "signed-out") && (s.user === null || typeof s.user === "object" && typeof s.user.login === "string");
39
30
  };
31
+ const channel = (0, import_pushChannel.createPushChannel)({
32
+ pushType: "auth-state",
33
+ requestType: "request-auth-state",
34
+ initial: { status: "unknown", user: null },
35
+ parse: (msg) => isAuthState(msg.state) ? msg.state : void 0
36
+ });
37
+ const getAuthState = () => channel.get();
38
+ const onAuthChange = (listener) => channel.onChange(listener);
39
+ const useAuth = () => channel.use();
40
40
  // Annotate the CommonJS export names for ESM import in node:
41
41
  0 && (module.exports = {
42
42
  getAuthState,
package/dist/auth.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auth.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n/**\n * Login / account state of the immediately.run user, mirrored from the host\n * window into the sandbox.\n *\n * `status` is `'unknown'` until the host has reported a value (use it to\n * distinguish \"still loading\" from a confirmed signed-out session).\n */\nexport type AuthStatus = 'unknown' | 'signed-in' | 'signed-out';\n\nexport interface SandboxUser {\n /** GitHub login (handle) of the signed-in user. */\n login: string;\n}\n\nexport interface AuthState {\n status: AuthStatus;\n user: SandboxUser | null;\n}\n\ninterface AuthService {\n getState(): AuthState;\n onChange(listener: (state: AuthState) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler` is the sandbox bundler injected into the\n// evaluation context (same path the other SDK helpers reach for `messageBus`).\nconst authService = (): AuthService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.auth;\n};\n\n/**\n * Returns the current login / account state. Poll this whenever you need a\n * one-off read; use {@link onAuthChange} or {@link useAuth} to react to changes.\n */\nexport const getAuthState = (): AuthState => authService().getState();\n\n/**\n * Subscribe to login / logout changes. The listener is invoked immediately with\n * the current state, then again on every change. Returns an unsubscribe fn.\n */\nexport const onAuthChange = (listener: (state: AuthState) => void): (() => void) => {\n const disposable = authService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * React hook returning the current login / account state, re-rendering on\n * login / logout.\n */\nexport const useAuth = (): AuthState => {\n const [state, setState] = useState<AuthState>(getAuthState);\n useEffect(() => onAuthChange(setState), []);\n return state;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAoC;AA4BpC,MAAM,cAAc,MAAmB;AAErC,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAMO,MAAM,eAAe,MAAiB,YAAY,EAAE,SAAS;AAM7D,MAAM,eAAe,CAAC,aAAuD;AAClF,QAAM,aAAa,YAAY,EAAE,SAAS,QAAQ;AAClD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAMO,MAAM,UAAU,MAAiB;AACtC,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAoB,YAAY;AAC1D,8BAAU,MAAM,aAAa,QAAQ,GAAG,CAAC,CAAC;AAC1C,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/auth.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\n\n/**\n * Login / account state of the immediately.run user, mirrored from the host\n * window into the sandbox.\n *\n * `status` is `'unknown'` until the host has reported a value (use it to\n * distinguish \"still loading\" from a confirmed signed-out session).\n */\nexport type AuthStatus = 'unknown' | 'signed-in' | 'signed-out';\n\nexport interface SandboxUser {\n /** GitHub login (handle) of the signed-in user. */\n login: string;\n}\n\nexport interface AuthState {\n status: AuthStatus;\n user: SandboxUser | null;\n}\n\nconst isAuthState = (v: unknown): v is AuthState => {\n const s = v as Partial<AuthState> | null;\n return (\n !!s &&\n (s.status === 'unknown' || s.status === 'signed-in' || s.status === 'signed-out') &&\n (s.user === null || (typeof s.user === 'object' && typeof (s.user as SandboxUser).login === 'string'))\n );\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `auth-state`\n// and answers `request-auth-state` (wire format: site-main channelBridge.ts).\nconst channel = createPushChannel<AuthState>({\n pushType: 'auth-state',\n requestType: 'request-auth-state',\n initial: { status: 'unknown', user: null },\n parse: (msg) => (isAuthState(msg.state) ? (msg.state as AuthState) : undefined),\n});\n\n/**\n * Returns the current login / account state. Poll this whenever you need a\n * one-off read; use {@link onAuthChange} or {@link useAuth} to react to changes.\n */\nexport const getAuthState = (): AuthState => channel.get();\n\n/**\n * Subscribe to login / logout changes. The listener is invoked immediately with\n * the current state, then again on every change. Returns an unsubscribe fn.\n */\nexport const onAuthChange = (listener: (state: AuthState) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current login / account state, re-rendering on\n * login / logout.\n */\nexport const useAuth = (): AuthState => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AAqBlC,MAAM,cAAc,CAAC,MAA+B;AAClD,QAAM,IAAI;AACV,SACE,CAAC,CAAC,MACD,EAAE,WAAW,aAAa,EAAE,WAAW,eAAe,EAAE,WAAW,kBACnE,EAAE,SAAS,QAAS,OAAO,EAAE,SAAS,YAAY,OAAQ,EAAE,KAAqB,UAAU;AAEhG;AAIA,MAAM,cAAU,sCAA6B;AAAA,EAC3C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,EAAE,QAAQ,WAAW,MAAM,KAAK;AAAA,EACzC,OAAO,CAAC,QAAS,YAAY,IAAI,KAAK,IAAK,IAAI,QAAsB;AACvE,CAAC;AAMM,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAMlD,MAAM,eAAe,CAAC,aAC3B,QAAQ,SAAS,QAAQ;AAMpB,MAAM,UAAU,MAAiB,QAAQ,IAAI;","names":[]}
package/dist/auth.js CHANGED
@@ -1,17 +1,17 @@
1
- import { useEffect, useState } from "react";
2
- const authService = () => {
3
- return module.evaluation.module.bundler.auth;
4
- };
5
- const getAuthState = () => authService().getState();
6
- const onAuthChange = (listener) => {
7
- const disposable = authService().onChange(listener);
8
- return () => disposable.dispose();
9
- };
10
- const useAuth = () => {
11
- const [state, setState] = useState(getAuthState);
12
- useEffect(() => onAuthChange(setState), []);
13
- return state;
1
+ import { createPushChannel } from "./pushChannel";
2
+ const isAuthState = (v) => {
3
+ const s = v;
4
+ return !!s && (s.status === "unknown" || s.status === "signed-in" || s.status === "signed-out") && (s.user === null || typeof s.user === "object" && typeof s.user.login === "string");
14
5
  };
6
+ const channel = createPushChannel({
7
+ pushType: "auth-state",
8
+ requestType: "request-auth-state",
9
+ initial: { status: "unknown", user: null },
10
+ parse: (msg) => isAuthState(msg.state) ? msg.state : void 0
11
+ });
12
+ const getAuthState = () => channel.get();
13
+ const onAuthChange = (listener) => channel.onChange(listener);
14
+ const useAuth = () => channel.use();
15
15
  export {
16
16
  getAuthState,
17
17
  onAuthChange,
package/dist/auth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auth.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n/**\n * Login / account state of the immediately.run user, mirrored from the host\n * window into the sandbox.\n *\n * `status` is `'unknown'` until the host has reported a value (use it to\n * distinguish \"still loading\" from a confirmed signed-out session).\n */\nexport type AuthStatus = 'unknown' | 'signed-in' | 'signed-out';\n\nexport interface SandboxUser {\n /** GitHub login (handle) of the signed-in user. */\n login: string;\n}\n\nexport interface AuthState {\n status: AuthStatus;\n user: SandboxUser | null;\n}\n\ninterface AuthService {\n getState(): AuthState;\n onChange(listener: (state: AuthState) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler` is the sandbox bundler injected into the\n// evaluation context (same path the other SDK helpers reach for `messageBus`).\nconst authService = (): AuthService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.auth;\n};\n\n/**\n * Returns the current login / account state. Poll this whenever you need a\n * one-off read; use {@link onAuthChange} or {@link useAuth} to react to changes.\n */\nexport const getAuthState = (): AuthState => authService().getState();\n\n/**\n * Subscribe to login / logout changes. The listener is invoked immediately with\n * the current state, then again on every change. Returns an unsubscribe fn.\n */\nexport const onAuthChange = (listener: (state: AuthState) => void): (() => void) => {\n const disposable = authService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * React hook returning the current login / account state, re-rendering on\n * login / logout.\n */\nexport const useAuth = (): AuthState => {\n const [state, setState] = useState<AuthState>(getAuthState);\n useEffect(() => onAuthChange(setState), []);\n return state;\n};\n"],"mappings":"AAAA,SAAS,WAAW,gBAAgB;AA4BpC,MAAM,cAAc,MAAmB;AAErC,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAMO,MAAM,eAAe,MAAiB,YAAY,EAAE,SAAS;AAM7D,MAAM,eAAe,CAAC,aAAuD;AAClF,QAAM,aAAa,YAAY,EAAE,SAAS,QAAQ;AAClD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAMO,MAAM,UAAU,MAAiB;AACtC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAoB,YAAY;AAC1D,YAAU,MAAM,aAAa,QAAQ,GAAG,CAAC,CAAC;AAC1C,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/auth.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\n\n/**\n * Login / account state of the immediately.run user, mirrored from the host\n * window into the sandbox.\n *\n * `status` is `'unknown'` until the host has reported a value (use it to\n * distinguish \"still loading\" from a confirmed signed-out session).\n */\nexport type AuthStatus = 'unknown' | 'signed-in' | 'signed-out';\n\nexport interface SandboxUser {\n /** GitHub login (handle) of the signed-in user. */\n login: string;\n}\n\nexport interface AuthState {\n status: AuthStatus;\n user: SandboxUser | null;\n}\n\nconst isAuthState = (v: unknown): v is AuthState => {\n const s = v as Partial<AuthState> | null;\n return (\n !!s &&\n (s.status === 'unknown' || s.status === 'signed-in' || s.status === 'signed-out') &&\n (s.user === null || (typeof s.user === 'object' && typeof (s.user as SandboxUser).login === 'string'))\n );\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `auth-state`\n// and answers `request-auth-state` (wire format: site-main channelBridge.ts).\nconst channel = createPushChannel<AuthState>({\n pushType: 'auth-state',\n requestType: 'request-auth-state',\n initial: { status: 'unknown', user: null },\n parse: (msg) => (isAuthState(msg.state) ? (msg.state as AuthState) : undefined),\n});\n\n/**\n * Returns the current login / account state. Poll this whenever you need a\n * one-off read; use {@link onAuthChange} or {@link useAuth} to react to changes.\n */\nexport const getAuthState = (): AuthState => channel.get();\n\n/**\n * Subscribe to login / logout changes. The listener is invoked immediately with\n * the current state, then again on every change. Returns an unsubscribe fn.\n */\nexport const onAuthChange = (listener: (state: AuthState) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current login / account state, re-rendering on\n * login / logout.\n */\nexport const useAuth = (): AuthState => channel.use();\n"],"mappings":"AAAA,SAAS,yBAAyB;AAqBlC,MAAM,cAAc,CAAC,MAA+B;AAClD,QAAM,IAAI;AACV,SACE,CAAC,CAAC,MACD,EAAE,WAAW,aAAa,EAAE,WAAW,eAAe,EAAE,WAAW,kBACnE,EAAE,SAAS,QAAS,OAAO,EAAE,SAAS,YAAY,OAAQ,EAAE,KAAqB,UAAU;AAEhG;AAIA,MAAM,UAAU,kBAA6B;AAAA,EAC3C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,EAAE,QAAQ,WAAW,MAAM,KAAK;AAAA,EACzC,OAAO,CAAC,QAAS,YAAY,IAAI,KAAK,IAAK,IAAI,QAAsB;AACvE,CAAC;AAMM,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAMlD,MAAM,eAAe,CAAC,aAC3B,QAAQ,SAAS,QAAQ;AAMpB,MAAM,UAAU,MAAiB,QAAQ,IAAI;","names":[]}
package/dist/catalog.cjs CHANGED
@@ -25,9 +25,9 @@ __export(catalog_exports, {
25
25
  useCatalog: () => useCatalog
26
26
  });
27
27
  module.exports = __toCommonJS(catalog_exports);
28
- var import_react = require("react");
29
28
  var import_sandboxUtils = require("./sandboxUtils");
30
29
  var import_protocolStream = require("./protocolStream");
30
+ var import_pushChannel = require("./pushChannel");
31
31
  const split = (name) => {
32
32
  const i = name.indexOf(":");
33
33
  if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);
@@ -43,35 +43,23 @@ const invoke = async (name, params = {}) => {
43
43
  }
44
44
  return res.data;
45
45
  };
46
- const bundlerTransport = {
47
- send: (msg) => (
48
- // @ts-ignore - injected by the sandbox runtime
49
- module.evaluation.module.bundler.messageBus.sendMessage(msg.type, msg)
50
- ),
51
- subscribe: (type, handler) => {
52
- const d = module.evaluation.module.bundler.messageBus.onMessage((m) => {
53
- if (m && m.type === type) handler(m);
54
- });
55
- return () => d.dispose();
56
- }
46
+ const streamTransport = {
47
+ send: (msg) => (0, import_sandboxUtils.sendMessage)(msg.type, msg),
48
+ subscribe: (type, handler) => (0, import_sandboxUtils.addListener)(type, (msg) => handler(msg))
57
49
  };
58
50
  function invokeStream(name, params = {}) {
59
51
  const [scheme, method] = split(name);
60
- return (0, import_protocolStream.consumeStream)(bundlerTransport, `protocol-${scheme}`, method, [params]);
52
+ return (0, import_protocolStream.consumeStream)(streamTransport, `protocol-${scheme}`, method, [params]);
61
53
  }
62
- const catalogService = () => {
63
- return module.evaluation.module.bundler.catalog;
64
- };
65
- const getCatalog = () => catalogService().getCatalog();
66
- const onCatalogChange = (listener) => {
67
- const disposable = catalogService().onChange(listener);
68
- return () => disposable.dispose();
69
- };
70
- const useCatalog = () => {
71
- const [catalog, setCatalog] = (0, import_react.useState)(getCatalog);
72
- (0, import_react.useEffect)(() => onCatalogChange(setCatalog), []);
73
- return catalog;
74
- };
54
+ const channel = (0, import_pushChannel.createPushChannel)({
55
+ pushType: "api-catalog",
56
+ requestType: "request-api-catalog",
57
+ initial: [],
58
+ parse: (msg) => Array.isArray(msg.methods) ? msg.methods : void 0
59
+ });
60
+ const getCatalog = () => channel.get();
61
+ const onCatalogChange = (listener) => channel.onChange(listener);
62
+ const useCatalog = () => channel.use();
75
63
  // Annotate the CommonJS export names for ESM import in node:
76
64
  0 && (module.exports = {
77
65
  getCatalog,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { useEffect, useState } from 'react';\nimport { protocolRequest } from './sandboxUtils';\nimport type { StreamFrame } from './protocolStream';\nimport { consumeStream } from './protocolStream';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\nconst bundlerTransport = {\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) =>\n // @ts-ignore - injected by the sandbox runtime\n module.evaluation.module.bundler.messageBus.sendMessage(msg.type, msg),\n subscribe: (type: string, handler: (msg: { msgId?: number; stream?: StreamFrame }) => void) => {\n // @ts-ignore - injected by the sandbox runtime\n const d = module.evaluation.module.bundler.messageBus.onMessage((m: { type?: string }) => {\n if (m && m.type === type) handler(m as { msgId?: number; stream?: StreamFrame });\n });\n return () => d.dispose();\n },\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(bundlerTransport, `protocol-${scheme}`, method, [params]);\n}\n\ninterface CatalogService {\n getCatalog(): ApiMethod[];\n onChange(listener: (catalog: ApiMethod[]) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler.catalog` injected by the sandbox runtime.\nconst catalogService = (): CatalogService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.catalog;\n};\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => catalogService().getCatalog();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) => {\n const disposable = catalogService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => {\n const [catalog, setCatalog] = useState<ApiMethod[]>(getCatalog);\n useEffect(() => onCatalogChange(setCatalog), []);\n return catalog;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,mBAAoC;AACpC,0BAAgC;AAEhC,4BAA8B;AAa9B,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,UAAM,qCAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAEA,MAAM,mBAAmB;AAAA,EACvB,MAAM,CAAC;AAAA;AAAA,IAEL,OAAO,WAAW,OAAO,QAAQ,WAAW,YAAY,IAAI,MAAM,GAAG;AAAA;AAAA,EACvE,WAAW,CAAC,MAAc,YAAqE;AAE7F,UAAM,IAAI,OAAO,WAAW,OAAO,QAAQ,WAAW,UAAU,CAAC,MAAyB;AACxF,UAAI,KAAK,EAAE,SAAS,KAAM,SAAQ,CAA6C;AAAA,IACjF,CAAC;AACD,WAAO,MAAM,EAAE,QAAQ;AAAA,EACzB;AACF;AAGO,SAAS,aACd,MACA,SAAkC,CAAC,GACP;AAC5B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AACnC,aAAO,qCAAoB,kBAAkB,YAAY,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;AACrF;AAQA,MAAM,iBAAiB,MAAsB;AAE3C,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAIO,MAAM,aAAa,MAAmB,eAAe,EAAE,WAAW;AAIlE,MAAM,kBAAkB,CAAC,aAA2D;AACzF,QAAM,aAAa,eAAe,EAAE,SAAS,QAAQ;AACrD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAKO,MAAM,aAAa,MAAmB;AAC3C,QAAM,CAAC,SAAS,UAAU,QAAI,uBAAsB,UAAU;AAC9D,8BAAU,MAAM,gBAAgB,UAAU,GAAG,CAAC,CAAC;AAC/C,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport type { StreamFrame, StreamTransport } from './protocolStream';\nimport { consumeStream } from './protocolStream';\nimport { createPushChannel } from './pushChannel';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Stream transport over the resolver (SDK_PACKAGING_SPEC §4) sendMessage /\n// addListener route through `transport()` (injected bundler messageBus or the §4\n// global), never `bundler.messageBus` directly.\nconst streamTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(streamTransport, `protocol-${scheme}`, method, [params]);\n}\n\n// The catalog list is read over the transport (§4): the host pushes `api-catalog`\n// and answers `request-api-catalog` with this app's grant-filtered methods (wire\n// format: site-main channelBridge.ts).\nconst channel = createPushChannel<ApiMethod[]>({\n pushType: 'api-catalog',\n requestType: 'request-api-catalog',\n initial: [],\n parse: (msg) => (Array.isArray(msg.methods) ? (msg.methods as ApiMethod[]) : undefined),\n});\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => channel.get();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,0BAA0D;AAE1D,4BAA8B;AAC9B,yBAAkC;AAalC,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,UAAM,qCAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,kBAAmC;AAAA,EACvC,MAAM,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,gBAChB,iCAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AACvF;AAGO,SAAS,aACd,MACA,SAAkC,CAAC,GACP;AAC5B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AACnC,aAAO,qCAAoB,iBAAiB,YAAY,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;AACpF;AAKA,MAAM,cAAU,sCAA+B;AAAA,EAC7C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA0B;AAC/E,CAAC;AAIM,MAAM,aAAa,MAAmB,QAAQ,IAAI;AAIlD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAKpB,MAAM,aAAa,MAAmB,QAAQ,IAAI;","names":[]}
package/dist/catalog.js CHANGED
@@ -1,6 +1,6 @@
1
- import { useEffect, useState } from "react";
2
- import { protocolRequest } from "./sandboxUtils";
1
+ import { protocolRequest, sendMessage, addListener } from "./sandboxUtils";
3
2
  import { consumeStream } from "./protocolStream";
3
+ import { createPushChannel } from "./pushChannel";
4
4
  const split = (name) => {
5
5
  const i = name.indexOf(":");
6
6
  if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);
@@ -16,35 +16,23 @@ const invoke = async (name, params = {}) => {
16
16
  }
17
17
  return res.data;
18
18
  };
19
- const bundlerTransport = {
20
- send: (msg) => (
21
- // @ts-ignore - injected by the sandbox runtime
22
- module.evaluation.module.bundler.messageBus.sendMessage(msg.type, msg)
23
- ),
24
- subscribe: (type, handler) => {
25
- const d = module.evaluation.module.bundler.messageBus.onMessage((m) => {
26
- if (m && m.type === type) handler(m);
27
- });
28
- return () => d.dispose();
29
- }
19
+ const streamTransport = {
20
+ send: (msg) => sendMessage(msg.type, msg),
21
+ subscribe: (type, handler) => addListener(type, (msg) => handler(msg))
30
22
  };
31
23
  function invokeStream(name, params = {}) {
32
24
  const [scheme, method] = split(name);
33
- return consumeStream(bundlerTransport, `protocol-${scheme}`, method, [params]);
25
+ return consumeStream(streamTransport, `protocol-${scheme}`, method, [params]);
34
26
  }
35
- const catalogService = () => {
36
- return module.evaluation.module.bundler.catalog;
37
- };
38
- const getCatalog = () => catalogService().getCatalog();
39
- const onCatalogChange = (listener) => {
40
- const disposable = catalogService().onChange(listener);
41
- return () => disposable.dispose();
42
- };
43
- const useCatalog = () => {
44
- const [catalog, setCatalog] = useState(getCatalog);
45
- useEffect(() => onCatalogChange(setCatalog), []);
46
- return catalog;
47
- };
27
+ const channel = createPushChannel({
28
+ pushType: "api-catalog",
29
+ requestType: "request-api-catalog",
30
+ initial: [],
31
+ parse: (msg) => Array.isArray(msg.methods) ? msg.methods : void 0
32
+ });
33
+ const getCatalog = () => channel.get();
34
+ const onCatalogChange = (listener) => channel.onChange(listener);
35
+ const useCatalog = () => channel.use();
48
36
  export {
49
37
  getCatalog,
50
38
  invoke,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { useEffect, useState } from 'react';\nimport { protocolRequest } from './sandboxUtils';\nimport type { StreamFrame } from './protocolStream';\nimport { consumeStream } from './protocolStream';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\nconst bundlerTransport = {\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) =>\n // @ts-ignore - injected by the sandbox runtime\n module.evaluation.module.bundler.messageBus.sendMessage(msg.type, msg),\n subscribe: (type: string, handler: (msg: { msgId?: number; stream?: StreamFrame }) => void) => {\n // @ts-ignore - injected by the sandbox runtime\n const d = module.evaluation.module.bundler.messageBus.onMessage((m: { type?: string }) => {\n if (m && m.type === type) handler(m as { msgId?: number; stream?: StreamFrame });\n });\n return () => d.dispose();\n },\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(bundlerTransport, `protocol-${scheme}`, method, [params]);\n}\n\ninterface CatalogService {\n getCatalog(): ApiMethod[];\n onChange(listener: (catalog: ApiMethod[]) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler.catalog` injected by the sandbox runtime.\nconst catalogService = (): CatalogService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.catalog;\n};\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => catalogService().getCatalog();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) => {\n const disposable = catalogService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => {\n const [catalog, setCatalog] = useState<ApiMethod[]>(getCatalog);\n useEffect(() => onCatalogChange(setCatalog), []);\n return catalog;\n};\n"],"mappings":"AAKA,SAAS,WAAW,gBAAgB;AACpC,SAAS,uBAAuB;AAEhC,SAAS,qBAAqB;AAa9B,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,MAAM,gBAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAEA,MAAM,mBAAmB;AAAA,EACvB,MAAM,CAAC;AAAA;AAAA,IAEL,OAAO,WAAW,OAAO,QAAQ,WAAW,YAAY,IAAI,MAAM,GAAG;AAAA;AAAA,EACvE,WAAW,CAAC,MAAc,YAAqE;AAE7F,UAAM,IAAI,OAAO,WAAW,OAAO,QAAQ,WAAW,UAAU,CAAC,MAAyB;AACxF,UAAI,KAAK,EAAE,SAAS,KAAM,SAAQ,CAA6C;AAAA,IACjF,CAAC;AACD,WAAO,MAAM,EAAE,QAAQ;AAAA,EACzB;AACF;AAGO,SAAS,aACd,MACA,SAAkC,CAAC,GACP;AAC5B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AACnC,SAAO,cAAoB,kBAAkB,YAAY,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;AACrF;AAQA,MAAM,iBAAiB,MAAsB;AAE3C,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAIO,MAAM,aAAa,MAAmB,eAAe,EAAE,WAAW;AAIlE,MAAM,kBAAkB,CAAC,aAA2D;AACzF,QAAM,aAAa,eAAe,EAAE,SAAS,QAAQ;AACrD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAKO,MAAM,aAAa,MAAmB;AAC3C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAsB,UAAU;AAC9D,YAAU,MAAM,gBAAgB,UAAU,GAAG,CAAC,CAAC;AAC/C,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport type { StreamFrame, StreamTransport } from './protocolStream';\nimport { consumeStream } from './protocolStream';\nimport { createPushChannel } from './pushChannel';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Stream transport over the resolver (SDK_PACKAGING_SPEC §4) sendMessage /\n// addListener route through `transport()` (injected bundler messageBus or the §4\n// global), never `bundler.messageBus` directly.\nconst streamTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(streamTransport, `protocol-${scheme}`, method, [params]);\n}\n\n// The catalog list is read over the transport (§4): the host pushes `api-catalog`\n// and answers `request-api-catalog` with this app's grant-filtered methods (wire\n// format: site-main channelBridge.ts).\nconst channel = createPushChannel<ApiMethod[]>({\n pushType: 'api-catalog',\n requestType: 'request-api-catalog',\n initial: [],\n parse: (msg) => (Array.isArray(msg.methods) ? (msg.methods as ApiMethod[]) : undefined),\n});\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => channel.get();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => channel.use();\n"],"mappings":"AAKA,SAAS,iBAAiB,aAAa,mBAAmB;AAE1D,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAalC,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,MAAM,gBAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,kBAAmC;AAAA,EACvC,MAAM,CAAC,QAAQ,YAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,YAChB,YAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AACvF;AAGO,SAAS,aACd,MACA,SAAkC,CAAC,GACP;AAC5B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AACnC,SAAO,cAAoB,iBAAiB,YAAY,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;AACpF;AAKA,MAAM,UAAU,kBAA+B;AAAA,EAC7C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA0B;AAC/E,CAAC;AAIM,MAAM,aAAa,MAAmB,QAAQ,IAAI;AAIlD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAKpB,MAAM,aAAa,MAAmB,QAAQ,IAAI;","names":[]}
@@ -23,20 +23,16 @@ __export(editorContext_exports, {
23
23
  useEditorContext: () => useEditorContext
24
24
  });
25
25
  module.exports = __toCommonJS(editorContext_exports);
26
- var import_react = require("react");
27
- const editorContextService = () => {
28
- return module.evaluation.module.bundler.editorContext;
29
- };
30
- const getEditorContext = () => editorContextService().getContext();
31
- const onEditorContextChange = (listener) => {
32
- const disposable = editorContextService().onChange(listener);
33
- return () => disposable.dispose();
34
- };
35
- const useEditorContext = () => {
36
- const [context, setContext] = (0, import_react.useState)(getEditorContext);
37
- (0, import_react.useEffect)(() => onEditorContextChange(setContext), []);
38
- return context;
39
- };
26
+ var import_pushChannel = require("./pushChannel");
27
+ const channel = (0, import_pushChannel.createPushChannel)({
28
+ pushType: "editor-context",
29
+ requestType: "request-editor-context",
30
+ initial: { dirtyPaths: [] },
31
+ parse: (msg) => Array.isArray(msg.dirtyPaths) && msg.dirtyPaths.every((p) => typeof p === "string") ? { dirtyPaths: msg.dirtyPaths } : void 0
32
+ });
33
+ const getEditorContext = () => channel.get();
34
+ const onEditorContextChange = (listener) => channel.onChange(listener);
35
+ const useEditorContext = () => channel.use();
40
36
  // Annotate the CommonJS export names for ESM import in node:
41
37
  0 && (module.exports = {
42
38
  getEditorContext,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n}\n\ninterface EditorContextService {\n getContext(): EditorContext;\n onChange(listener: (context: EditorContext) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler.editorContext` is the sandbox bundler service\n// injected into the evaluation context (same path the other SDK helpers use).\nconst editorContextService = (): EditorContextService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.editorContext;\n};\n\n/**\n * Returns the current editor context (dirty set). Poll this for a one-off read;\n * use {@link onEditorContextChange} or {@link useEditorContext} to react.\n */\nexport const getEditorContext = (): EditorContext => editorContextService().getContext();\n\n/**\n * Subscribe to editor-context changes. The listener is invoked immediately with\n * the current context, then again on every change. Returns an unsubscribe fn.\n */\nexport const onEditorContextChange = (\n listener: (context: EditorContext) => void,\n): (() => void) => {\n const disposable = editorContextService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * React hook returning the current editor context (dirty set), re-rendering when\n * it changes. Handy for a contribute dialog: `const { dirtyPaths } =\n * useEditorContext()` to show \"you'll save N files\" before calling `contribute()`.\n */\nexport const useEditorContext = (): EditorContext => {\n const [context, setContext] = useState<EditorContext>(getEditorContext);\n useEffect(() => onEditorContextChange(setContext), []);\n return context;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAoC;AAwBpC,MAAM,uBAAuB,MAA4B;AAEvD,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAMO,MAAM,mBAAmB,MAAqB,qBAAqB,EAAE,WAAW;AAMhF,MAAM,wBAAwB,CACnC,aACiB;AACjB,QAAM,aAAa,qBAAqB,EAAE,SAAS,QAAQ;AAC3D,SAAO,MAAM,WAAW,QAAQ;AAClC;AAOO,MAAM,mBAAmB,MAAqB;AACnD,QAAM,CAAC,SAAS,UAAU,QAAI,uBAAwB,gBAAgB;AACtE,8BAAU,MAAM,sBAAsB,UAAU,GAAG,CAAC,CAAC;AACrD,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n}\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `editor-context`\n// and answers `request-editor-context` but only for a frame holding `editor:read`\n// (gated by the channel router). An app without it gets no reply, so the empty\n// default below stands. Wire format: site-main channelBridge.ts.\nconst channel = createPushChannel<EditorContext>({\n pushType: 'editor-context',\n requestType: 'request-editor-context',\n initial: { dirtyPaths: [] },\n parse: (msg) =>\n Array.isArray(msg.dirtyPaths) && msg.dirtyPaths.every((p) => typeof p === 'string')\n ? { dirtyPaths: msg.dirtyPaths as string[] }\n : undefined,\n});\n\n/**\n * Returns the current editor context (dirty set). Poll this for a one-off read;\n * use {@link onEditorContextChange} or {@link useEditorContext} to react.\n */\nexport const getEditorContext = (): EditorContext => channel.get();\n\n/**\n * Subscribe to editor-context changes. The listener is invoked immediately with\n * the current context, then again on every change. Returns an unsubscribe fn.\n */\nexport const onEditorContextChange = (listener: (context: EditorContext) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current editor context (dirty set), re-rendering when\n * it changes. Handy for a contribute dialog: `const { dirtyPaths } =\n * useEditorContext()` to show \"you'll save N files\" before calling `contribute()`.\n */\nexport const useEditorContext = (): EditorContext => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AAqBlC,MAAM,cAAU,sCAAiC;AAAA,EAC/C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,EAAE,YAAY,CAAC,EAAE;AAAA,EAC1B,OAAO,CAAC,QACN,MAAM,QAAQ,IAAI,UAAU,KAAK,IAAI,WAAW,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ,IAC9E,EAAE,YAAY,IAAI,WAAuB,IACzC;AACR,CAAC;AAMM,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;AAM1D,MAAM,wBAAwB,CAAC,aACpC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;","names":[]}
@@ -1,17 +1,13 @@
1
- import { useEffect, useState } from "react";
2
- const editorContextService = () => {
3
- return module.evaluation.module.bundler.editorContext;
4
- };
5
- const getEditorContext = () => editorContextService().getContext();
6
- const onEditorContextChange = (listener) => {
7
- const disposable = editorContextService().onChange(listener);
8
- return () => disposable.dispose();
9
- };
10
- const useEditorContext = () => {
11
- const [context, setContext] = useState(getEditorContext);
12
- useEffect(() => onEditorContextChange(setContext), []);
13
- return context;
14
- };
1
+ import { createPushChannel } from "./pushChannel";
2
+ const channel = createPushChannel({
3
+ pushType: "editor-context",
4
+ requestType: "request-editor-context",
5
+ initial: { dirtyPaths: [] },
6
+ parse: (msg) => Array.isArray(msg.dirtyPaths) && msg.dirtyPaths.every((p) => typeof p === "string") ? { dirtyPaths: msg.dirtyPaths } : void 0
7
+ });
8
+ const getEditorContext = () => channel.get();
9
+ const onEditorContextChange = (listener) => channel.onChange(listener);
10
+ const useEditorContext = () => channel.use();
15
11
  export {
16
12
  getEditorContext,
17
13
  onEditorContextChange,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n}\n\ninterface EditorContextService {\n getContext(): EditorContext;\n onChange(listener: (context: EditorContext) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler.editorContext` is the sandbox bundler service\n// injected into the evaluation context (same path the other SDK helpers use).\nconst editorContextService = (): EditorContextService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.editorContext;\n};\n\n/**\n * Returns the current editor context (dirty set). Poll this for a one-off read;\n * use {@link onEditorContextChange} or {@link useEditorContext} to react.\n */\nexport const getEditorContext = (): EditorContext => editorContextService().getContext();\n\n/**\n * Subscribe to editor-context changes. The listener is invoked immediately with\n * the current context, then again on every change. Returns an unsubscribe fn.\n */\nexport const onEditorContextChange = (\n listener: (context: EditorContext) => void,\n): (() => void) => {\n const disposable = editorContextService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * React hook returning the current editor context (dirty set), re-rendering when\n * it changes. Handy for a contribute dialog: `const { dirtyPaths } =\n * useEditorContext()` to show \"you'll save N files\" before calling `contribute()`.\n */\nexport const useEditorContext = (): EditorContext => {\n const [context, setContext] = useState<EditorContext>(getEditorContext);\n useEffect(() => onEditorContextChange(setContext), []);\n return context;\n};\n"],"mappings":"AAAA,SAAS,WAAW,gBAAgB;AAwBpC,MAAM,uBAAuB,MAA4B;AAEvD,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAMO,MAAM,mBAAmB,MAAqB,qBAAqB,EAAE,WAAW;AAMhF,MAAM,wBAAwB,CACnC,aACiB;AACjB,QAAM,aAAa,qBAAqB,EAAE,SAAS,QAAQ;AAC3D,SAAO,MAAM,WAAW,QAAQ;AAClC;AAOO,MAAM,mBAAmB,MAAqB;AACnD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAwB,gBAAgB;AACtE,YAAU,MAAM,sBAAsB,UAAU,GAAG,CAAC,CAAC;AACrD,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n}\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `editor-context`\n// and answers `request-editor-context` but only for a frame holding `editor:read`\n// (gated by the channel router). An app without it gets no reply, so the empty\n// default below stands. Wire format: site-main channelBridge.ts.\nconst channel = createPushChannel<EditorContext>({\n pushType: 'editor-context',\n requestType: 'request-editor-context',\n initial: { dirtyPaths: [] },\n parse: (msg) =>\n Array.isArray(msg.dirtyPaths) && msg.dirtyPaths.every((p) => typeof p === 'string')\n ? { dirtyPaths: msg.dirtyPaths as string[] }\n : undefined,\n});\n\n/**\n * Returns the current editor context (dirty set). Poll this for a one-off read;\n * use {@link onEditorContextChange} or {@link useEditorContext} to react.\n */\nexport const getEditorContext = (): EditorContext => channel.get();\n\n/**\n * Subscribe to editor-context changes. The listener is invoked immediately with\n * the current context, then again on every change. Returns an unsubscribe fn.\n */\nexport const onEditorContextChange = (listener: (context: EditorContext) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current editor context (dirty set), re-rendering when\n * it changes. Handy for a contribute dialog: `const { dirtyPaths } =\n * useEditorContext()` to show \"you'll save N files\" before calling `contribute()`.\n */\nexport const useEditorContext = (): EditorContext => channel.use();\n"],"mappings":"AAAA,SAAS,yBAAyB;AAqBlC,MAAM,UAAU,kBAAiC;AAAA,EAC/C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,EAAE,YAAY,CAAC,EAAE;AAAA,EAC1B,OAAO,CAAC,QACN,MAAM,QAAQ,IAAI,UAAU,KAAK,IAAI,WAAW,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ,IAC9E,EAAE,YAAY,IAAI,WAAuB,IACzC;AACR,CAAC;AAMM,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;AAM1D,MAAM,wBAAwB,CAAC,aACpC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;","names":[]}
@@ -23,20 +23,26 @@ __export(formFactor_exports, {
23
23
  useFormFactor: () => useFormFactor
24
24
  });
25
25
  module.exports = __toCommonJS(formFactor_exports);
26
- var import_react = require("react");
27
- const formFactorService = () => {
28
- return module.evaluation.module.bundler.formFactor;
26
+ var import_pushChannel = require("./pushChannel");
27
+ const DEFAULT_FORM_FACTOR = {
28
+ class: "desktop",
29
+ orientation: "landscape",
30
+ width: 1280,
31
+ height: 800
29
32
  };
30
- const getFormFactor = () => formFactorService().getFormFactor();
31
- const onFormFactorChange = (listener) => {
32
- const disposable = formFactorService().onChange(listener);
33
- return () => disposable.dispose();
34
- };
35
- const useFormFactor = () => {
36
- const [ff, setFf] = (0, import_react.useState)(getFormFactor);
37
- (0, import_react.useEffect)(() => onFormFactorChange(setFf), []);
38
- return ff;
33
+ const isFormFactor = (v) => {
34
+ const f = v;
35
+ return !!f && (f.class === "mobile" || f.class === "tablet" || f.class === "desktop") && (f.orientation === "portrait" || f.orientation === "landscape") && typeof f.width === "number" && typeof f.height === "number";
39
36
  };
37
+ const channel = (0, import_pushChannel.createPushChannel)({
38
+ pushType: "form-factor",
39
+ requestType: "request-form-factor",
40
+ initial: DEFAULT_FORM_FACTOR,
41
+ parse: (msg) => isFormFactor(msg.formFactor) ? msg.formFactor : void 0
42
+ });
43
+ const getFormFactor = () => channel.get();
44
+ const onFormFactorChange = (listener) => channel.onChange(listener);
45
+ const useFormFactor = () => channel.use();
40
46
  // Annotate the CommonJS export names for ESM import in node:
41
47
  0 && (module.exports = {
42
48
  getFormFactor,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/formFactor.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n/**\n * The form factor of the surface your app is rendered into, mirrored from the\n * immediately.run host (UI_AS_APPS_SPEC §5.4.1). Read this to lay out\n * responsively — a narrow chrome panel, a full preview, or a mobile carousel\n * pane all report their box here. The host is the source of truth (it owns the\n * region); you cannot reliably measure your own viewport across the sandbox\n * boundary.\n *\n * Baseline capability `formFactor:read` — every app may read it.\n */\nexport type FormFactorClass = 'mobile' | 'tablet' | 'desktop';\nexport type Orientation = 'portrait' | 'landscape';\n\nexport interface FormFactor {\n class: FormFactorClass;\n orientation: Orientation;\n width: number;\n height: number;\n}\n\ninterface FormFactorService {\n getFormFactor(): FormFactor;\n onChange(listener: (formFactor: FormFactor) => void): { dispose(): void };\n}\n\nconst formFactorService = (): FormFactorService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.formFactor;\n};\n\n/** Returns the current form factor. Poll for a one-off read. */\nexport const getFormFactor = (): FormFactor =>\n formFactorService().getFormFactor();\n\n/**\n * Subscribe to form-factor changes. The listener is invoked immediately with\n * the current value, then again on every change. Returns an unsubscribe fn.\n */\nexport const onFormFactorChange = (\n listener: (formFactor: FormFactor) => void,\n): (() => void) => {\n const disposable = formFactorService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/** React hook returning the current form factor, re-rendering on change. */\nexport const useFormFactor = (): FormFactor => {\n const [ff, setFf] = useState<FormFactor>(getFormFactor);\n useEffect(() => onFormFactorChange(setFf), []);\n return ff;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAoC;AA2BpC,MAAM,oBAAoB,MAAyB;AAEjD,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAGO,MAAM,gBAAgB,MAC3B,kBAAkB,EAAE,cAAc;AAM7B,MAAM,qBAAqB,CAChC,aACiB;AACjB,QAAM,aAAa,kBAAkB,EAAE,SAAS,QAAQ;AACxD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAGO,MAAM,gBAAgB,MAAkB;AAC7C,QAAM,CAAC,IAAI,KAAK,QAAI,uBAAqB,aAAa;AACtD,8BAAU,MAAM,mBAAmB,KAAK,GAAG,CAAC,CAAC;AAC7C,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/formFactor.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\n\n/**\n * The form factor of the surface your app is rendered into, mirrored from the\n * immediately.run host (UI_AS_APPS_SPEC §5.4.1). Read this to lay out\n * responsively — a narrow chrome panel, a full preview, or a mobile carousel\n * pane all report their box here. The host is the source of truth (it owns the\n * region); you cannot reliably measure your own viewport across the sandbox\n * boundary.\n *\n * Baseline capability `formFactor:read` — every app may read it.\n */\nexport type FormFactorClass = 'mobile' | 'tablet' | 'desktop';\nexport type Orientation = 'portrait' | 'landscape';\n\nexport interface FormFactor {\n class: FormFactorClass;\n orientation: Orientation;\n width: number;\n height: number;\n}\n\n/** Assumed before the host reports — a reasonable desktop default. */\nconst DEFAULT_FORM_FACTOR: FormFactor = {\n class: 'desktop',\n orientation: 'landscape',\n width: 1280,\n height: 800,\n};\n\nconst isFormFactor = (v: unknown): v is FormFactor => {\n const f = v as Partial<FormFactor> | null;\n return (\n !!f &&\n (f.class === 'mobile' || f.class === 'tablet' || f.class === 'desktop') &&\n (f.orientation === 'portrait' || f.orientation === 'landscape') &&\n typeof f.width === 'number' &&\n typeof f.height === 'number'\n );\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `form-factor`\n// and answers `request-form-factor` (wire format: site-main channelBridge.ts).\nconst channel = createPushChannel<FormFactor>({\n pushType: 'form-factor',\n requestType: 'request-form-factor',\n initial: DEFAULT_FORM_FACTOR,\n parse: (msg) => (isFormFactor(msg.formFactor) ? (msg.formFactor as FormFactor) : undefined),\n});\n\n/** Returns the current form factor. Poll for a one-off read. */\nexport const getFormFactor = (): FormFactor => channel.get();\n\n/**\n * Subscribe to form-factor changes. The listener is invoked immediately with\n * the current value, then again on every change. Returns an unsubscribe fn.\n */\nexport const onFormFactorChange = (listener: (formFactor: FormFactor) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning the current form factor, re-rendering on change. */\nexport const useFormFactor = (): FormFactor => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AAuBlC,MAAM,sBAAkC;AAAA,EACtC,OAAO;AAAA,EACP,aAAa;AAAA,EACb,OAAO;AAAA,EACP,QAAQ;AACV;AAEA,MAAM,eAAe,CAAC,MAAgC;AACpD,QAAM,IAAI;AACV,SACE,CAAC,CAAC,MACD,EAAE,UAAU,YAAY,EAAE,UAAU,YAAY,EAAE,UAAU,eAC5D,EAAE,gBAAgB,cAAc,EAAE,gBAAgB,gBACnD,OAAO,EAAE,UAAU,YACnB,OAAO,EAAE,WAAW;AAExB;AAIA,MAAM,cAAU,sCAA8B;AAAA,EAC5C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,aAAa,IAAI,UAAU,IAAK,IAAI,aAA4B;AACnF,CAAC;AAGM,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;AAMpD,MAAM,qBAAqB,CAAC,aACjC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;","names":[]}
@@ -1,17 +1,23 @@
1
- import { useEffect, useState } from "react";
2
- const formFactorService = () => {
3
- return module.evaluation.module.bundler.formFactor;
1
+ import { createPushChannel } from "./pushChannel";
2
+ const DEFAULT_FORM_FACTOR = {
3
+ class: "desktop",
4
+ orientation: "landscape",
5
+ width: 1280,
6
+ height: 800
4
7
  };
5
- const getFormFactor = () => formFactorService().getFormFactor();
6
- const onFormFactorChange = (listener) => {
7
- const disposable = formFactorService().onChange(listener);
8
- return () => disposable.dispose();
9
- };
10
- const useFormFactor = () => {
11
- const [ff, setFf] = useState(getFormFactor);
12
- useEffect(() => onFormFactorChange(setFf), []);
13
- return ff;
8
+ const isFormFactor = (v) => {
9
+ const f = v;
10
+ return !!f && (f.class === "mobile" || f.class === "tablet" || f.class === "desktop") && (f.orientation === "portrait" || f.orientation === "landscape") && typeof f.width === "number" && typeof f.height === "number";
14
11
  };
12
+ const channel = createPushChannel({
13
+ pushType: "form-factor",
14
+ requestType: "request-form-factor",
15
+ initial: DEFAULT_FORM_FACTOR,
16
+ parse: (msg) => isFormFactor(msg.formFactor) ? msg.formFactor : void 0
17
+ });
18
+ const getFormFactor = () => channel.get();
19
+ const onFormFactorChange = (listener) => channel.onChange(listener);
20
+ const useFormFactor = () => channel.use();
15
21
  export {
16
22
  getFormFactor,
17
23
  onFormFactorChange,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/formFactor.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n/**\n * The form factor of the surface your app is rendered into, mirrored from the\n * immediately.run host (UI_AS_APPS_SPEC §5.4.1). Read this to lay out\n * responsively — a narrow chrome panel, a full preview, or a mobile carousel\n * pane all report their box here. The host is the source of truth (it owns the\n * region); you cannot reliably measure your own viewport across the sandbox\n * boundary.\n *\n * Baseline capability `formFactor:read` — every app may read it.\n */\nexport type FormFactorClass = 'mobile' | 'tablet' | 'desktop';\nexport type Orientation = 'portrait' | 'landscape';\n\nexport interface FormFactor {\n class: FormFactorClass;\n orientation: Orientation;\n width: number;\n height: number;\n}\n\ninterface FormFactorService {\n getFormFactor(): FormFactor;\n onChange(listener: (formFactor: FormFactor) => void): { dispose(): void };\n}\n\nconst formFactorService = (): FormFactorService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.formFactor;\n};\n\n/** Returns the current form factor. Poll for a one-off read. */\nexport const getFormFactor = (): FormFactor =>\n formFactorService().getFormFactor();\n\n/**\n * Subscribe to form-factor changes. The listener is invoked immediately with\n * the current value, then again on every change. Returns an unsubscribe fn.\n */\nexport const onFormFactorChange = (\n listener: (formFactor: FormFactor) => void,\n): (() => void) => {\n const disposable = formFactorService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/** React hook returning the current form factor, re-rendering on change. */\nexport const useFormFactor = (): FormFactor => {\n const [ff, setFf] = useState<FormFactor>(getFormFactor);\n useEffect(() => onFormFactorChange(setFf), []);\n return ff;\n};\n"],"mappings":"AAAA,SAAS,WAAW,gBAAgB;AA2BpC,MAAM,oBAAoB,MAAyB;AAEjD,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAGO,MAAM,gBAAgB,MAC3B,kBAAkB,EAAE,cAAc;AAM7B,MAAM,qBAAqB,CAChC,aACiB;AACjB,QAAM,aAAa,kBAAkB,EAAE,SAAS,QAAQ;AACxD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAGO,MAAM,gBAAgB,MAAkB;AAC7C,QAAM,CAAC,IAAI,KAAK,IAAI,SAAqB,aAAa;AACtD,YAAU,MAAM,mBAAmB,KAAK,GAAG,CAAC,CAAC;AAC7C,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/formFactor.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\n\n/**\n * The form factor of the surface your app is rendered into, mirrored from the\n * immediately.run host (UI_AS_APPS_SPEC §5.4.1). Read this to lay out\n * responsively — a narrow chrome panel, a full preview, or a mobile carousel\n * pane all report their box here. The host is the source of truth (it owns the\n * region); you cannot reliably measure your own viewport across the sandbox\n * boundary.\n *\n * Baseline capability `formFactor:read` — every app may read it.\n */\nexport type FormFactorClass = 'mobile' | 'tablet' | 'desktop';\nexport type Orientation = 'portrait' | 'landscape';\n\nexport interface FormFactor {\n class: FormFactorClass;\n orientation: Orientation;\n width: number;\n height: number;\n}\n\n/** Assumed before the host reports — a reasonable desktop default. */\nconst DEFAULT_FORM_FACTOR: FormFactor = {\n class: 'desktop',\n orientation: 'landscape',\n width: 1280,\n height: 800,\n};\n\nconst isFormFactor = (v: unknown): v is FormFactor => {\n const f = v as Partial<FormFactor> | null;\n return (\n !!f &&\n (f.class === 'mobile' || f.class === 'tablet' || f.class === 'desktop') &&\n (f.orientation === 'portrait' || f.orientation === 'landscape') &&\n typeof f.width === 'number' &&\n typeof f.height === 'number'\n );\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `form-factor`\n// and answers `request-form-factor` (wire format: site-main channelBridge.ts).\nconst channel = createPushChannel<FormFactor>({\n pushType: 'form-factor',\n requestType: 'request-form-factor',\n initial: DEFAULT_FORM_FACTOR,\n parse: (msg) => (isFormFactor(msg.formFactor) ? (msg.formFactor as FormFactor) : undefined),\n});\n\n/** Returns the current form factor. Poll for a one-off read. */\nexport const getFormFactor = (): FormFactor => channel.get();\n\n/**\n * Subscribe to form-factor changes. The listener is invoked immediately with\n * the current value, then again on every change. Returns an unsubscribe fn.\n */\nexport const onFormFactorChange = (listener: (formFactor: FormFactor) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning the current form factor, re-rendering on change. */\nexport const useFormFactor = (): FormFactor => channel.use();\n"],"mappings":"AAAA,SAAS,yBAAyB;AAuBlC,MAAM,sBAAkC;AAAA,EACtC,OAAO;AAAA,EACP,aAAa;AAAA,EACb,OAAO;AAAA,EACP,QAAQ;AACV;AAEA,MAAM,eAAe,CAAC,MAAgC;AACpD,QAAM,IAAI;AACV,SACE,CAAC,CAAC,MACD,EAAE,UAAU,YAAY,EAAE,UAAU,YAAY,EAAE,UAAU,eAC5D,EAAE,gBAAgB,cAAc,EAAE,gBAAgB,gBACnD,OAAO,EAAE,UAAU,YACnB,OAAO,EAAE,WAAW;AAExB;AAIA,MAAM,UAAU,kBAA8B;AAAA,EAC5C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,aAAa,IAAI,UAAU,IAAK,IAAI,aAA4B;AACnF,CAAC;AAGM,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;AAMpD,MAAM,qBAAqB,CAAC,aACjC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;","names":[]}
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var pushChannel_exports = {};
20
+ __export(pushChannel_exports, {
21
+ createPushChannel: () => createPushChannel
22
+ });
23
+ module.exports = __toCommonJS(pushChannel_exports);
24
+ var import_react = require("react");
25
+ var import_sandboxUtils = require("./sandboxUtils");
26
+ function createPushChannel(opts, transport = { sendMessage: import_sandboxUtils.sendMessage, addListener: import_sandboxUtils.addListener }) {
27
+ let current = opts.initial;
28
+ const listeners = /* @__PURE__ */ new Set();
29
+ let started = false;
30
+ const start = () => {
31
+ if (started) return;
32
+ started = true;
33
+ transport.addListener(opts.pushType, (msg) => {
34
+ const next = opts.parse(msg);
35
+ if (next !== void 0) {
36
+ current = next;
37
+ listeners.forEach((l) => l(current));
38
+ }
39
+ });
40
+ if (opts.requestType) {
41
+ try {
42
+ transport.sendMessage(opts.requestType);
43
+ } catch {
44
+ }
45
+ }
46
+ };
47
+ const get = () => {
48
+ start();
49
+ return current;
50
+ };
51
+ const onChange = (listener) => {
52
+ start();
53
+ listeners.add(listener);
54
+ listener(current);
55
+ return () => {
56
+ listeners.delete(listener);
57
+ };
58
+ };
59
+ const use = () => {
60
+ const [value, setValue] = (0, import_react.useState)(get);
61
+ (0, import_react.useEffect)(() => onChange(setValue), []);
62
+ return value;
63
+ };
64
+ return { get, onChange, use };
65
+ }
66
+ // Annotate the CommonJS export names for ESM import in node:
67
+ 0 && (module.exports = {
68
+ createPushChannel
69
+ });
70
+ //# sourceMappingURL=pushChannel.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pushChannel.ts"],"sourcesContent":["// Generic host→sandbox state channel read over the TRANSPORT\n// (SDK_PACKAGING_SPEC §4, expose-transport). The host pushes a `pushType`\n// message whenever the value changes and answers a `requestType` poll with the\n// current value — gated per-frame by the read ACL (site-main channelBridge.ts /\n// channelRouter). Historically the SDK read these values off injected sandbox\n// service objects (`module.evaluation.module.bundler.auth` etc.); this caches\n// the latest value SDK-side instead, so the SDK is self-contained and the\n// bundler services can eventually be retired.\n//\n// Every state helper (formFactor, auth, editorContext, theme, catalog) is a thin\n// wrapper over one of these — the get / onChange / use trio is identical.\nimport { useEffect, useState } from 'react';\nimport { sendMessage as defaultSend, addListener as defaultAddListener } from './sandboxUtils';\n\nexport interface PushChannel<T> {\n /** Pollable snapshot of the current value. */\n get(): T;\n /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */\n onChange(listener: (value: T) => void): () => void;\n /** React hook returning the current value, re-rendering on change. */\n use(): T;\n}\n\n/** Injectable transport — defaults to the real one; overridden in tests. */\nexport interface ChannelTransport {\n sendMessage: (type: string, data?: Record<string, unknown>) => void;\n addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;\n}\n\nexport function createPushChannel<T>(\n opts: {\n /** Host→sandbox push message type (e.g. `form-factor`). */\n pushType: string;\n /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */\n requestType?: string;\n /** Value assumed before the host answers — also the value when the app may not read the channel. */\n initial: T;\n /** Extract + validate the value from a push message; return `undefined` to ignore the message. */\n parse: (msg: Record<string, unknown>) => T | undefined;\n },\n transport: ChannelTransport = { sendMessage: defaultSend, addListener: defaultAddListener },\n): PushChannel<T> {\n let current = opts.initial;\n const listeners = new Set<(value: T) => void>();\n let started = false;\n\n // Lazily start on first read: register the push listener + send one poll, so a\n // late-mounting app still gets the current value. A channel the app may not\n // read is simply never answered → `initial` stands.\n const start = () => {\n if (started) return;\n started = true;\n transport.addListener(opts.pushType, (msg) => {\n const next = opts.parse(msg);\n if (next !== undefined) {\n current = next;\n listeners.forEach((l) => l(current));\n }\n });\n if (opts.requestType) {\n try {\n transport.sendMessage(opts.requestType);\n } catch {\n /* transport not ready — a proactive push will still arrive */\n }\n }\n };\n\n const get = (): T => {\n start();\n return current;\n };\n\n const onChange = (listener: (value: T) => void): (() => void) => {\n start();\n listeners.add(listener);\n listener(current);\n return () => {\n listeners.delete(listener);\n };\n };\n\n const use = (): T => {\n const [value, setValue] = useState<T>(get);\n useEffect(() => onChange(setValue), []);\n return value;\n };\n\n return { get, onChange, use };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,mBAAoC;AACpC,0BAA8E;AAiBvE,SAAS,kBACd,MAUA,YAA8B,EAAE,aAAa,oBAAAA,aAAa,aAAa,oBAAAC,YAAmB,GAC1E;AAChB,MAAI,UAAU,KAAK;AACnB,QAAM,YAAY,oBAAI,IAAwB;AAC9C,MAAI,UAAU;AAKd,QAAM,QAAQ,MAAM;AAClB,QAAI,QAAS;AACb,cAAU;AACV,cAAU,YAAY,KAAK,UAAU,CAAC,QAAQ;AAC5C,YAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,UAAI,SAAS,QAAW;AACtB,kBAAU;AACV,kBAAU,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AAAA,MACrC;AAAA,IACF,CAAC;AACD,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,kBAAU,YAAY,KAAK,WAAW;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM;AACN,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,CAAC,aAA+C;AAC/D,UAAM;AACN,cAAU,IAAI,QAAQ;AACtB,aAAS,OAAO;AAChB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM,CAAC,OAAO,QAAQ,QAAI,uBAAY,GAAG;AACzC,gCAAU,MAAM,SAAS,QAAQ,GAAG,CAAC,CAAC;AACtC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,UAAU,IAAI;AAC9B;","names":["defaultSend","defaultAddListener"]}
@@ -0,0 +1,25 @@
1
+ interface PushChannel<T> {
2
+ /** Pollable snapshot of the current value. */
3
+ get(): T;
4
+ /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */
5
+ onChange(listener: (value: T) => void): () => void;
6
+ /** React hook returning the current value, re-rendering on change. */
7
+ use(): T;
8
+ }
9
+ /** Injectable transport — defaults to the real one; overridden in tests. */
10
+ interface ChannelTransport {
11
+ sendMessage: (type: string, data?: Record<string, unknown>) => void;
12
+ addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;
13
+ }
14
+ declare function createPushChannel<T>(opts: {
15
+ /** Host→sandbox push message type (e.g. `form-factor`). */
16
+ pushType: string;
17
+ /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */
18
+ requestType?: string;
19
+ /** Value assumed before the host answers — also the value when the app may not read the channel. */
20
+ initial: T;
21
+ /** Extract + validate the value from a push message; return `undefined` to ignore the message. */
22
+ parse: (msg: Record<string, unknown>) => T | undefined;
23
+ }, transport?: ChannelTransport): PushChannel<T>;
24
+
25
+ export { type ChannelTransport, type PushChannel, createPushChannel };
@@ -0,0 +1,25 @@
1
+ interface PushChannel<T> {
2
+ /** Pollable snapshot of the current value. */
3
+ get(): T;
4
+ /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */
5
+ onChange(listener: (value: T) => void): () => void;
6
+ /** React hook returning the current value, re-rendering on change. */
7
+ use(): T;
8
+ }
9
+ /** Injectable transport — defaults to the real one; overridden in tests. */
10
+ interface ChannelTransport {
11
+ sendMessage: (type: string, data?: Record<string, unknown>) => void;
12
+ addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;
13
+ }
14
+ declare function createPushChannel<T>(opts: {
15
+ /** Host→sandbox push message type (e.g. `form-factor`). */
16
+ pushType: string;
17
+ /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */
18
+ requestType?: string;
19
+ /** Value assumed before the host answers — also the value when the app may not read the channel. */
20
+ initial: T;
21
+ /** Extract + validate the value from a push message; return `undefined` to ignore the message. */
22
+ parse: (msg: Record<string, unknown>) => T | undefined;
23
+ }, transport?: ChannelTransport): PushChannel<T>;
24
+
25
+ export { type ChannelTransport, type PushChannel, createPushChannel };
@@ -0,0 +1,46 @@
1
+ import { useEffect, useState } from "react";
2
+ import { sendMessage as defaultSend, addListener as defaultAddListener } from "./sandboxUtils";
3
+ function createPushChannel(opts, transport = { sendMessage: defaultSend, addListener: defaultAddListener }) {
4
+ let current = opts.initial;
5
+ const listeners = /* @__PURE__ */ new Set();
6
+ let started = false;
7
+ const start = () => {
8
+ if (started) return;
9
+ started = true;
10
+ transport.addListener(opts.pushType, (msg) => {
11
+ const next = opts.parse(msg);
12
+ if (next !== void 0) {
13
+ current = next;
14
+ listeners.forEach((l) => l(current));
15
+ }
16
+ });
17
+ if (opts.requestType) {
18
+ try {
19
+ transport.sendMessage(opts.requestType);
20
+ } catch {
21
+ }
22
+ }
23
+ };
24
+ const get = () => {
25
+ start();
26
+ return current;
27
+ };
28
+ const onChange = (listener) => {
29
+ start();
30
+ listeners.add(listener);
31
+ listener(current);
32
+ return () => {
33
+ listeners.delete(listener);
34
+ };
35
+ };
36
+ const use = () => {
37
+ const [value, setValue] = useState(get);
38
+ useEffect(() => onChange(setValue), []);
39
+ return value;
40
+ };
41
+ return { get, onChange, use };
42
+ }
43
+ export {
44
+ createPushChannel
45
+ };
46
+ //# sourceMappingURL=pushChannel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pushChannel.ts"],"sourcesContent":["// Generic host→sandbox state channel read over the TRANSPORT\n// (SDK_PACKAGING_SPEC §4, expose-transport). The host pushes a `pushType`\n// message whenever the value changes and answers a `requestType` poll with the\n// current value — gated per-frame by the read ACL (site-main channelBridge.ts /\n// channelRouter). Historically the SDK read these values off injected sandbox\n// service objects (`module.evaluation.module.bundler.auth` etc.); this caches\n// the latest value SDK-side instead, so the SDK is self-contained and the\n// bundler services can eventually be retired.\n//\n// Every state helper (formFactor, auth, editorContext, theme, catalog) is a thin\n// wrapper over one of these — the get / onChange / use trio is identical.\nimport { useEffect, useState } from 'react';\nimport { sendMessage as defaultSend, addListener as defaultAddListener } from './sandboxUtils';\n\nexport interface PushChannel<T> {\n /** Pollable snapshot of the current value. */\n get(): T;\n /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */\n onChange(listener: (value: T) => void): () => void;\n /** React hook returning the current value, re-rendering on change. */\n use(): T;\n}\n\n/** Injectable transport — defaults to the real one; overridden in tests. */\nexport interface ChannelTransport {\n sendMessage: (type: string, data?: Record<string, unknown>) => void;\n addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;\n}\n\nexport function createPushChannel<T>(\n opts: {\n /** Host→sandbox push message type (e.g. `form-factor`). */\n pushType: string;\n /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */\n requestType?: string;\n /** Value assumed before the host answers — also the value when the app may not read the channel. */\n initial: T;\n /** Extract + validate the value from a push message; return `undefined` to ignore the message. */\n parse: (msg: Record<string, unknown>) => T | undefined;\n },\n transport: ChannelTransport = { sendMessage: defaultSend, addListener: defaultAddListener },\n): PushChannel<T> {\n let current = opts.initial;\n const listeners = new Set<(value: T) => void>();\n let started = false;\n\n // Lazily start on first read: register the push listener + send one poll, so a\n // late-mounting app still gets the current value. A channel the app may not\n // read is simply never answered → `initial` stands.\n const start = () => {\n if (started) return;\n started = true;\n transport.addListener(opts.pushType, (msg) => {\n const next = opts.parse(msg);\n if (next !== undefined) {\n current = next;\n listeners.forEach((l) => l(current));\n }\n });\n if (opts.requestType) {\n try {\n transport.sendMessage(opts.requestType);\n } catch {\n /* transport not ready — a proactive push will still arrive */\n }\n }\n };\n\n const get = (): T => {\n start();\n return current;\n };\n\n const onChange = (listener: (value: T) => void): (() => void) => {\n start();\n listeners.add(listener);\n listener(current);\n return () => {\n listeners.delete(listener);\n };\n };\n\n const use = (): T => {\n const [value, setValue] = useState<T>(get);\n useEffect(() => onChange(setValue), []);\n return value;\n };\n\n return { get, onChange, use };\n}\n"],"mappings":"AAWA,SAAS,WAAW,gBAAgB;AACpC,SAAS,eAAe,aAAa,eAAe,0BAA0B;AAiBvE,SAAS,kBACd,MAUA,YAA8B,EAAE,aAAa,aAAa,aAAa,mBAAmB,GAC1E;AAChB,MAAI,UAAU,KAAK;AACnB,QAAM,YAAY,oBAAI,IAAwB;AAC9C,MAAI,UAAU;AAKd,QAAM,QAAQ,MAAM;AAClB,QAAI,QAAS;AACb,cAAU;AACV,cAAU,YAAY,KAAK,UAAU,CAAC,QAAQ;AAC5C,YAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,UAAI,SAAS,QAAW;AACtB,kBAAU;AACV,kBAAU,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AAAA,MACrC;AAAA,IACF,CAAC;AACD,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,kBAAU,YAAY,KAAK,WAAW;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM;AACN,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,CAAC,aAA+C;AAC/D,UAAM;AACN,cAAU,IAAI,QAAQ;AACtB,aAAS,OAAO;AAChB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM,CAAC,OAAO,QAAQ,IAAI,SAAY,GAAG;AACzC,cAAU,MAAM,SAAS,QAAQ,GAAG,CAAC,CAAC;AACtC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,UAAU,IAAI;AAC9B;","names":[]}
package/dist/runtime.cjs CHANGED
@@ -28,7 +28,7 @@ module.exports = __toCommonJS(runtime_exports);
28
28
  var import_sandboxUtils = require("./sandboxUtils");
29
29
  var import_hostRuntime = require("./hostRuntime");
30
30
  const SDK_PROTOCOL_VERSION = "1.0.0";
31
- const SDK_VERSION = "0.2.8";
31
+ const SDK_VERSION = "0.3.0";
32
32
  const sdkHandshake = () => ({
33
33
  sdkVersion: SDK_VERSION,
34
34
  protocolVersion: SDK_PROTOCOL_VERSION
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["// Runtime discovery + version handshake (SDK_PACKAGING_SPEC §4/§6).\n//\n// Today the SDK reaches the host through the INJECTED sandbox services\n// (`module.evaluation.module.bundler.*`, see sandboxUtils). The packaging migration\n// makes the SDK an app-pinnable npm dependency that finds the runtime through a\n// stable, versioned global the sandbox publishes BEFORE evaluating app code:\n//\n// globalThis.__immediatelyRun__ = { runtimeVersion, protocolVersion, transport }\n//\n// Phase 1 (behind a flag, injection still active): the SDK can READ that global\n// when present (else fall back to injection), and ANNOUNCE its own version +\n// protocol so the host can record + version-check it (§6/T45). The transport itself\n// is unchanged here — this only wires the discovery + handshake fields so the check\n// exists when app-pinned versions become real.\nimport { sendMessage, addListener } from './sandboxUtils';\n\n// `getHostRuntime` + `ImmediatelyRunGlobal` live in the leaf `hostRuntime` module\n// (imports nothing) and are re-exported here for a stable public API. This breaks\n// the sandboxUtils↔runtime import cycle: sandboxUtils reads `getHostRuntime` from\n// the leaf, while runtime still imports sandboxUtils for the handshake — one\n// direction only, no cycle.\nexport { getHostRuntime } from './hostRuntime';\nexport type { ImmediatelyRunGlobal } from './hostRuntime';\n\n/** The wire protocol (postMessage envelope / channels / methods) THIS SDK speaks.\n * Additive-only (§9); bump only for a backwards-compatible extension. */\nexport const SDK_PROTOCOL_VERSION = '1.0.0';\n\n/** This SDK's package version. Kept in step with package.json (a build step can\n * inject it later; a constant is fine while versions are still effectively fixed). */\nexport const SDK_VERSION = '0.2.8';\n\n/** This SDK's handshake payload — the version + protocol the host records + checks\n * against `HOST_PROTOCOL_VERSION` (§6/T45). */\nexport interface SdkHandshake {\n sdkVersion: string;\n protocolVersion: string;\n}\nexport const sdkHandshake = (): SdkHandshake => ({\n sdkVersion: SDK_VERSION,\n protocolVersion: SDK_PROTOCOL_VERSION,\n});\n\n/**\n * Announce this SDK's version to the host (§6). Sends `sdk-handshake` eagerly\n * (best-effort — the host may already be listening) AND replies to a host\n * `request-handshake` (the robust path, mirroring the other `request-*` pulls).\n * Idempotent; safe to call more than once. Returns an unsubscribe fn.\n */\nexport function announceHandshake(): () => void {\n const send = () => {\n try {\n sendMessage('sdk-handshake', sdkHandshake() as unknown as Record<string, unknown>);\n } catch {\n /* transport not ready yet — the request-handshake reply covers it */\n }\n };\n send();\n return addListener('request-handshake', send);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,0BAAyC;AAOzC,yBAA+B;AAKxB,MAAM,uBAAuB;AAI7B,MAAM,cAAc;AAQpB,MAAM,eAAe,OAAqB;AAAA,EAC/C,YAAY;AAAA,EACZ,iBAAiB;AACnB;AAQO,SAAS,oBAAgC;AAC9C,QAAM,OAAO,MAAM;AACjB,QAAI;AACF,2CAAY,iBAAiB,aAAa,CAAuC;AAAA,IACnF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,OAAK;AACL,aAAO,iCAAY,qBAAqB,IAAI;AAC9C;","names":[]}
1
+ {"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["// Runtime discovery + version handshake (SDK_PACKAGING_SPEC §4/§6).\n//\n// Today the SDK reaches the host through the INJECTED sandbox services\n// (`module.evaluation.module.bundler.*`, see sandboxUtils). The packaging migration\n// makes the SDK an app-pinnable npm dependency that finds the runtime through a\n// stable, versioned global the sandbox publishes BEFORE evaluating app code:\n//\n// globalThis.__immediatelyRun__ = { runtimeVersion, protocolVersion, transport }\n//\n// Phase 1 (behind a flag, injection still active): the SDK can READ that global\n// when present (else fall back to injection), and ANNOUNCE its own version +\n// protocol so the host can record + version-check it (§6/T45). The transport itself\n// is unchanged here — this only wires the discovery + handshake fields so the check\n// exists when app-pinned versions become real.\nimport { sendMessage, addListener } from './sandboxUtils';\n\n// `getHostRuntime` + `ImmediatelyRunGlobal` live in the leaf `hostRuntime` module\n// (imports nothing) and are re-exported here for a stable public API. This breaks\n// the sandboxUtils↔runtime import cycle: sandboxUtils reads `getHostRuntime` from\n// the leaf, while runtime still imports sandboxUtils for the handshake — one\n// direction only, no cycle.\nexport { getHostRuntime } from './hostRuntime';\nexport type { ImmediatelyRunGlobal } from './hostRuntime';\n\n/** The wire protocol (postMessage envelope / channels / methods) THIS SDK speaks.\n * Additive-only (§9); bump only for a backwards-compatible extension. */\nexport const SDK_PROTOCOL_VERSION = '1.0.0';\n\n/** This SDK's package version. Kept in step with package.json (a build step can\n * inject it later; a constant is fine while versions are still effectively fixed). */\nexport const SDK_VERSION = '0.3.0';\n\n/** This SDK's handshake payload — the version + protocol the host records + checks\n * against `HOST_PROTOCOL_VERSION` (§6/T45). */\nexport interface SdkHandshake {\n sdkVersion: string;\n protocolVersion: string;\n}\nexport const sdkHandshake = (): SdkHandshake => ({\n sdkVersion: SDK_VERSION,\n protocolVersion: SDK_PROTOCOL_VERSION,\n});\n\n/**\n * Announce this SDK's version to the host (§6). Sends `sdk-handshake` eagerly\n * (best-effort — the host may already be listening) AND replies to a host\n * `request-handshake` (the robust path, mirroring the other `request-*` pulls).\n * Idempotent; safe to call more than once. Returns an unsubscribe fn.\n */\nexport function announceHandshake(): () => void {\n const send = () => {\n try {\n sendMessage('sdk-handshake', sdkHandshake() as unknown as Record<string, unknown>);\n } catch {\n /* transport not ready yet — the request-handshake reply covers it */\n }\n };\n send();\n return addListener('request-handshake', send);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,0BAAyC;AAOzC,yBAA+B;AAKxB,MAAM,uBAAuB;AAI7B,MAAM,cAAc;AAQpB,MAAM,eAAe,OAAqB;AAAA,EAC/C,YAAY;AAAA,EACZ,iBAAiB;AACnB;AAQO,SAAS,oBAAgC;AAC9C,QAAM,OAAO,MAAM;AACjB,QAAI;AACF,2CAAY,iBAAiB,aAAa,CAAuC;AAAA,IACnF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,OAAK;AACL,aAAO,iCAAY,qBAAqB,IAAI;AAC9C;","names":[]}
@@ -5,7 +5,7 @@ export { ImmediatelyRunGlobal, getHostRuntime } from './hostRuntime.cjs';
5
5
  declare const SDK_PROTOCOL_VERSION = "1.0.0";
6
6
  /** This SDK's package version. Kept in step with package.json (a build step can
7
7
  * inject it later; a constant is fine while versions are still effectively fixed). */
8
- declare const SDK_VERSION = "0.2.8";
8
+ declare const SDK_VERSION = "0.3.0";
9
9
  /** This SDK's handshake payload — the version + protocol the host records + checks
10
10
  * against `HOST_PROTOCOL_VERSION` (§6/T45). */
11
11
  interface SdkHandshake {
package/dist/runtime.d.ts CHANGED
@@ -5,7 +5,7 @@ export { ImmediatelyRunGlobal, getHostRuntime } from './hostRuntime.js';
5
5
  declare const SDK_PROTOCOL_VERSION = "1.0.0";
6
6
  /** This SDK's package version. Kept in step with package.json (a build step can
7
7
  * inject it later; a constant is fine while versions are still effectively fixed). */
8
- declare const SDK_VERSION = "0.2.8";
8
+ declare const SDK_VERSION = "0.3.0";
9
9
  /** This SDK's handshake payload — the version + protocol the host records + checks
10
10
  * against `HOST_PROTOCOL_VERSION` (§6/T45). */
11
11
  interface SdkHandshake {
package/dist/runtime.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { sendMessage, addListener } from "./sandboxUtils";
2
2
  import { getHostRuntime } from "./hostRuntime";
3
3
  const SDK_PROTOCOL_VERSION = "1.0.0";
4
- const SDK_VERSION = "0.2.8";
4
+ const SDK_VERSION = "0.3.0";
5
5
  const sdkHandshake = () => ({
6
6
  sdkVersion: SDK_VERSION,
7
7
  protocolVersion: SDK_PROTOCOL_VERSION
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["// Runtime discovery + version handshake (SDK_PACKAGING_SPEC §4/§6).\n//\n// Today the SDK reaches the host through the INJECTED sandbox services\n// (`module.evaluation.module.bundler.*`, see sandboxUtils). The packaging migration\n// makes the SDK an app-pinnable npm dependency that finds the runtime through a\n// stable, versioned global the sandbox publishes BEFORE evaluating app code:\n//\n// globalThis.__immediatelyRun__ = { runtimeVersion, protocolVersion, transport }\n//\n// Phase 1 (behind a flag, injection still active): the SDK can READ that global\n// when present (else fall back to injection), and ANNOUNCE its own version +\n// protocol so the host can record + version-check it (§6/T45). The transport itself\n// is unchanged here — this only wires the discovery + handshake fields so the check\n// exists when app-pinned versions become real.\nimport { sendMessage, addListener } from './sandboxUtils';\n\n// `getHostRuntime` + `ImmediatelyRunGlobal` live in the leaf `hostRuntime` module\n// (imports nothing) and are re-exported here for a stable public API. This breaks\n// the sandboxUtils↔runtime import cycle: sandboxUtils reads `getHostRuntime` from\n// the leaf, while runtime still imports sandboxUtils for the handshake — one\n// direction only, no cycle.\nexport { getHostRuntime } from './hostRuntime';\nexport type { ImmediatelyRunGlobal } from './hostRuntime';\n\n/** The wire protocol (postMessage envelope / channels / methods) THIS SDK speaks.\n * Additive-only (§9); bump only for a backwards-compatible extension. */\nexport const SDK_PROTOCOL_VERSION = '1.0.0';\n\n/** This SDK's package version. Kept in step with package.json (a build step can\n * inject it later; a constant is fine while versions are still effectively fixed). */\nexport const SDK_VERSION = '0.2.8';\n\n/** This SDK's handshake payload — the version + protocol the host records + checks\n * against `HOST_PROTOCOL_VERSION` (§6/T45). */\nexport interface SdkHandshake {\n sdkVersion: string;\n protocolVersion: string;\n}\nexport const sdkHandshake = (): SdkHandshake => ({\n sdkVersion: SDK_VERSION,\n protocolVersion: SDK_PROTOCOL_VERSION,\n});\n\n/**\n * Announce this SDK's version to the host (§6). Sends `sdk-handshake` eagerly\n * (best-effort — the host may already be listening) AND replies to a host\n * `request-handshake` (the robust path, mirroring the other `request-*` pulls).\n * Idempotent; safe to call more than once. Returns an unsubscribe fn.\n */\nexport function announceHandshake(): () => void {\n const send = () => {\n try {\n sendMessage('sdk-handshake', sdkHandshake() as unknown as Record<string, unknown>);\n } catch {\n /* transport not ready yet — the request-handshake reply covers it */\n }\n };\n send();\n return addListener('request-handshake', send);\n}\n"],"mappings":"AAcA,SAAS,aAAa,mBAAmB;AAOzC,SAAS,sBAAsB;AAKxB,MAAM,uBAAuB;AAI7B,MAAM,cAAc;AAQpB,MAAM,eAAe,OAAqB;AAAA,EAC/C,YAAY;AAAA,EACZ,iBAAiB;AACnB;AAQO,SAAS,oBAAgC;AAC9C,QAAM,OAAO,MAAM;AACjB,QAAI;AACF,kBAAY,iBAAiB,aAAa,CAAuC;AAAA,IACnF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,OAAK;AACL,SAAO,YAAY,qBAAqB,IAAI;AAC9C;","names":[]}
1
+ {"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["// Runtime discovery + version handshake (SDK_PACKAGING_SPEC §4/§6).\n//\n// Today the SDK reaches the host through the INJECTED sandbox services\n// (`module.evaluation.module.bundler.*`, see sandboxUtils). The packaging migration\n// makes the SDK an app-pinnable npm dependency that finds the runtime through a\n// stable, versioned global the sandbox publishes BEFORE evaluating app code:\n//\n// globalThis.__immediatelyRun__ = { runtimeVersion, protocolVersion, transport }\n//\n// Phase 1 (behind a flag, injection still active): the SDK can READ that global\n// when present (else fall back to injection), and ANNOUNCE its own version +\n// protocol so the host can record + version-check it (§6/T45). The transport itself\n// is unchanged here — this only wires the discovery + handshake fields so the check\n// exists when app-pinned versions become real.\nimport { sendMessage, addListener } from './sandboxUtils';\n\n// `getHostRuntime` + `ImmediatelyRunGlobal` live in the leaf `hostRuntime` module\n// (imports nothing) and are re-exported here for a stable public API. This breaks\n// the sandboxUtils↔runtime import cycle: sandboxUtils reads `getHostRuntime` from\n// the leaf, while runtime still imports sandboxUtils for the handshake — one\n// direction only, no cycle.\nexport { getHostRuntime } from './hostRuntime';\nexport type { ImmediatelyRunGlobal } from './hostRuntime';\n\n/** The wire protocol (postMessage envelope / channels / methods) THIS SDK speaks.\n * Additive-only (§9); bump only for a backwards-compatible extension. */\nexport const SDK_PROTOCOL_VERSION = '1.0.0';\n\n/** This SDK's package version. Kept in step with package.json (a build step can\n * inject it later; a constant is fine while versions are still effectively fixed). */\nexport const SDK_VERSION = '0.3.0';\n\n/** This SDK's handshake payload — the version + protocol the host records + checks\n * against `HOST_PROTOCOL_VERSION` (§6/T45). */\nexport interface SdkHandshake {\n sdkVersion: string;\n protocolVersion: string;\n}\nexport const sdkHandshake = (): SdkHandshake => ({\n sdkVersion: SDK_VERSION,\n protocolVersion: SDK_PROTOCOL_VERSION,\n});\n\n/**\n * Announce this SDK's version to the host (§6). Sends `sdk-handshake` eagerly\n * (best-effort — the host may already be listening) AND replies to a host\n * `request-handshake` (the robust path, mirroring the other `request-*` pulls).\n * Idempotent; safe to call more than once. Returns an unsubscribe fn.\n */\nexport function announceHandshake(): () => void {\n const send = () => {\n try {\n sendMessage('sdk-handshake', sdkHandshake() as unknown as Record<string, unknown>);\n } catch {\n /* transport not ready yet — the request-handshake reply covers it */\n }\n };\n send();\n return addListener('request-handshake', send);\n}\n"],"mappings":"AAcA,SAAS,aAAa,mBAAmB;AAOzC,SAAS,sBAAsB;AAKxB,MAAM,uBAAuB;AAI7B,MAAM,cAAc;AAQpB,MAAM,eAAe,OAAqB;AAAA,EAC/C,YAAY;AAAA,EACZ,iBAAiB;AACnB;AAQO,SAAS,oBAAgC;AAC9C,QAAM,OAAO,MAAM;AACjB,QAAI;AACF,kBAAY,iBAAiB,aAAa,CAAuC;AAAA,IACnF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,OAAK;AACL,SAAO,YAAY,qBAAqB,IAAI;AAC9C;","names":[]}
package/dist/theme.cjs CHANGED
@@ -24,21 +24,17 @@ __export(theme_exports, {
24
24
  useHostTheme: () => useHostTheme
25
25
  });
26
26
  module.exports = __toCommonJS(theme_exports);
27
- var import_react = require("react");
27
+ var import_pushChannel = require("./pushChannel");
28
28
  var import_sandboxUtils = require("./sandboxUtils");
29
- const themeService = () => {
30
- return module.evaluation.module.bundler.theme;
31
- };
32
- const getHostTheme = () => themeService().getTheme();
33
- const onHostThemeChange = (listener) => {
34
- const disposable = themeService().onChange(listener);
35
- return () => disposable.dispose();
36
- };
37
- const useHostTheme = () => {
38
- const [theme, setTheme] = (0, import_react.useState)(getHostTheme);
39
- (0, import_react.useEffect)(() => onHostThemeChange(setTheme), []);
40
- return theme;
41
- };
29
+ const channel = (0, import_pushChannel.createPushChannel)({
30
+ pushType: "theme",
31
+ requestType: "request-theme",
32
+ initial: "dark",
33
+ parse: (msg) => msg.theme === "light" || msg.theme === "dark" ? msg.theme : void 0
34
+ });
35
+ const getHostTheme = () => channel.get();
36
+ const onHostThemeChange = (listener) => channel.onChange(listener);
37
+ const useHostTheme = () => channel.use();
42
38
  const setHostTheme = async (theme) => {
43
39
  const res = await (0, import_sandboxUtils.protocolRequest)("theme", "set", [{ theme }]);
44
40
  if (!res || res.ok !== true) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/theme.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { protocolRequest } from './sandboxUtils';\n\n/**\n * The host UI theme, mirrored from the immediately.run host window into the\n * sandbox. Your app can read this to render in step with the host chrome\n * (light / dark).\n *\n * This is the baseline `theme:read` capability — every app may read it. Changing\n * the host theme is a separate, elevated action (`theme:set`), available only to\n * the theme-toggle system app.\n */\nexport type HostTheme = 'light' | 'dark';\n\ninterface ThemeService {\n getTheme(): HostTheme;\n onChange(listener: (theme: HostTheme) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler.theme` is the sandbox bundler injected into\n// the evaluation context (same path the other SDK helpers use for `auth`).\nconst themeService = (): ThemeService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.theme;\n};\n\n/**\n * Returns the current host theme. Poll this for a one-off read; use\n * {@link onHostThemeChange} or {@link useHostTheme} to react to changes.\n */\nexport const getHostTheme = (): HostTheme => themeService().getTheme();\n\n/**\n * Subscribe to host theme changes. The listener is invoked immediately with the\n * current theme, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostThemeChange = (\n listener: (theme: HostTheme) => void,\n): (() => void) => {\n const disposable = themeService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * React hook returning the current host theme, re-rendering when it changes.\n * The recommended way to implement an app's own `useTheme`: follow the host,\n * allow a local override.\n */\nexport const useHostTheme = (): HostTheme => {\n const [theme, setTheme] = useState<HostTheme>(getHostTheme);\n useEffect(() => onHostThemeChange(setTheme), []);\n return theme;\n};\n\n/**\n * Set the host UI theme — the ELEVATED `theme:set` action (§8.5). The host\n * applies it and re-pushes the new value to every `theme:read` iframe, so your\n * own {@link useHostTheme} confirms the change (the loop closes with no special\n * case). Only a grant holding `theme:set` (e.g. the theme-toggle system app) may\n * call this; any other app is rejected host-side with a `forbidden`\n * {@link Error} (carrying `.code`), regardless of what the app claims. Update\n * optimistically and let the re-push confirm.\n */\nexport const setHostTheme = async (theme: HostTheme): Promise<void> => {\n const res = (await protocolRequest('theme', 'set', [{ theme }])) as\n | { ok: true; data?: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'setHostTheme failed') as Error & {\n code?: string;\n };\n err.code = (res && 'code' in res ? res.code : undefined) ?? 'unknown';\n throw err;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAoC;AACpC,0BAAgC;AAoBhC,MAAM,eAAe,MAAoB;AAEvC,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAMO,MAAM,eAAe,MAAiB,aAAa,EAAE,SAAS;AAM9D,MAAM,oBAAoB,CAC/B,aACiB;AACjB,QAAM,aAAa,aAAa,EAAE,SAAS,QAAQ;AACnD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAOO,MAAM,eAAe,MAAiB;AAC3C,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAoB,YAAY;AAC1D,8BAAU,MAAM,kBAAkB,QAAQ,GAAG,CAAC,CAAC;AAC/C,SAAO;AACT;AAWO,MAAM,eAAe,OAAO,UAAoC;AACrE,QAAM,MAAO,UAAM,qCAAgB,SAAS,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAI9D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,qBAAqB;AAG3D,QAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,OAAO,WAAc;AAC5D,UAAM;AAAA,EACR;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/theme.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\n\n/**\n * The host UI theme, mirrored from the immediately.run host window into the\n * sandbox. Your app can read this to render in step with the host chrome\n * (light / dark).\n *\n * This is the baseline `theme:read` capability — every app may read it. Changing\n * the host theme is a separate, elevated action (`theme:set`), available only to\n * the theme-toggle system app.\n */\nexport type HostTheme = 'light' | 'dark';\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `theme` and\n// answers `request-theme` (wire format: site-main channelBridge.ts). The host's\n// default before it reports is `dark` (sandbox themeState.DEFAULT_THEME).\nconst channel = createPushChannel<HostTheme>({\n pushType: 'theme',\n requestType: 'request-theme',\n initial: 'dark',\n parse: (msg) => (msg.theme === 'light' || msg.theme === 'dark' ? msg.theme : undefined),\n});\n\n/**\n * Returns the current host theme. Poll this for a one-off read; use\n * {@link onHostThemeChange} or {@link useHostTheme} to react to changes.\n */\nexport const getHostTheme = (): HostTheme => channel.get();\n\n/**\n * Subscribe to host theme changes. The listener is invoked immediately with the\n * current theme, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostThemeChange = (listener: (theme: HostTheme) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current host theme, re-rendering when it changes.\n * The recommended way to implement an app's own `useTheme`: follow the host,\n * allow a local override.\n */\nexport const useHostTheme = (): HostTheme => channel.use();\n\n/**\n * Set the host UI theme — the ELEVATED `theme:set` action (§8.5). The host\n * applies it and re-pushes the new value to every `theme:read` iframe, so your\n * own {@link useHostTheme} confirms the change (the loop closes with no special\n * case). Only a grant holding `theme:set` (e.g. the theme-toggle system app) may\n * call this; any other app is rejected host-side with a `forbidden`\n * {@link Error} (carrying `.code`), regardless of what the app claims. Update\n * optimistically and let the re-push confirm.\n */\nexport const setHostTheme = async (theme: HostTheme): Promise<void> => {\n const res = (await protocolRequest('theme', 'set', [{ theme }])) as\n | { ok: true; data?: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'setHostTheme failed') as Error & {\n code?: string;\n };\n err.code = (res && 'code' in res ? res.code : undefined) ?? 'unknown';\n throw err;\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AAClC,0BAAgC;AAgBhC,MAAM,cAAU,sCAA6B;AAAA,EAC3C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,IAAI,UAAU,WAAW,IAAI,UAAU,SAAS,IAAI,QAAQ;AAC/E,CAAC;AAMM,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAMlD,MAAM,oBAAoB,CAAC,aAChC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAWlD,MAAM,eAAe,OAAO,UAAoC;AACrE,QAAM,MAAO,UAAM,qCAAgB,SAAS,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAI9D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,qBAAqB;AAG3D,QAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,OAAO,WAAc;AAC5D,UAAM;AAAA,EACR;AACF;","names":[]}
package/dist/theme.js CHANGED
@@ -1,18 +1,14 @@
1
- import { useEffect, useState } from "react";
1
+ import { createPushChannel } from "./pushChannel";
2
2
  import { protocolRequest } from "./sandboxUtils";
3
- const themeService = () => {
4
- return module.evaluation.module.bundler.theme;
5
- };
6
- const getHostTheme = () => themeService().getTheme();
7
- const onHostThemeChange = (listener) => {
8
- const disposable = themeService().onChange(listener);
9
- return () => disposable.dispose();
10
- };
11
- const useHostTheme = () => {
12
- const [theme, setTheme] = useState(getHostTheme);
13
- useEffect(() => onHostThemeChange(setTheme), []);
14
- return theme;
15
- };
3
+ const channel = createPushChannel({
4
+ pushType: "theme",
5
+ requestType: "request-theme",
6
+ initial: "dark",
7
+ parse: (msg) => msg.theme === "light" || msg.theme === "dark" ? msg.theme : void 0
8
+ });
9
+ const getHostTheme = () => channel.get();
10
+ const onHostThemeChange = (listener) => channel.onChange(listener);
11
+ const useHostTheme = () => channel.use();
16
12
  const setHostTheme = async (theme) => {
17
13
  const res = await protocolRequest("theme", "set", [{ theme }]);
18
14
  if (!res || res.ok !== true) {
package/dist/theme.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/theme.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { protocolRequest } from './sandboxUtils';\n\n/**\n * The host UI theme, mirrored from the immediately.run host window into the\n * sandbox. Your app can read this to render in step with the host chrome\n * (light / dark).\n *\n * This is the baseline `theme:read` capability — every app may read it. Changing\n * the host theme is a separate, elevated action (`theme:set`), available only to\n * the theme-toggle system app.\n */\nexport type HostTheme = 'light' | 'dark';\n\ninterface ThemeService {\n getTheme(): HostTheme;\n onChange(listener: (theme: HostTheme) => void): { dispose(): void };\n}\n\n// `module.evaluation.module.bundler.theme` is the sandbox bundler injected into\n// the evaluation context (same path the other SDK helpers use for `auth`).\nconst themeService = (): ThemeService => {\n // @ts-ignore - injected by the sandbox runtime\n return module.evaluation.module.bundler.theme;\n};\n\n/**\n * Returns the current host theme. Poll this for a one-off read; use\n * {@link onHostThemeChange} or {@link useHostTheme} to react to changes.\n */\nexport const getHostTheme = (): HostTheme => themeService().getTheme();\n\n/**\n * Subscribe to host theme changes. The listener is invoked immediately with the\n * current theme, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostThemeChange = (\n listener: (theme: HostTheme) => void,\n): (() => void) => {\n const disposable = themeService().onChange(listener);\n return () => disposable.dispose();\n};\n\n/**\n * React hook returning the current host theme, re-rendering when it changes.\n * The recommended way to implement an app's own `useTheme`: follow the host,\n * allow a local override.\n */\nexport const useHostTheme = (): HostTheme => {\n const [theme, setTheme] = useState<HostTheme>(getHostTheme);\n useEffect(() => onHostThemeChange(setTheme), []);\n return theme;\n};\n\n/**\n * Set the host UI theme — the ELEVATED `theme:set` action (§8.5). The host\n * applies it and re-pushes the new value to every `theme:read` iframe, so your\n * own {@link useHostTheme} confirms the change (the loop closes with no special\n * case). Only a grant holding `theme:set` (e.g. the theme-toggle system app) may\n * call this; any other app is rejected host-side with a `forbidden`\n * {@link Error} (carrying `.code`), regardless of what the app claims. Update\n * optimistically and let the re-push confirm.\n */\nexport const setHostTheme = async (theme: HostTheme): Promise<void> => {\n const res = (await protocolRequest('theme', 'set', [{ theme }])) as\n | { ok: true; data?: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'setHostTheme failed') as Error & {\n code?: string;\n };\n err.code = (res && 'code' in res ? res.code : undefined) ?? 'unknown';\n throw err;\n }\n};\n"],"mappings":"AAAA,SAAS,WAAW,gBAAgB;AACpC,SAAS,uBAAuB;AAoBhC,MAAM,eAAe,MAAoB;AAEvC,SAAO,OAAO,WAAW,OAAO,QAAQ;AAC1C;AAMO,MAAM,eAAe,MAAiB,aAAa,EAAE,SAAS;AAM9D,MAAM,oBAAoB,CAC/B,aACiB;AACjB,QAAM,aAAa,aAAa,EAAE,SAAS,QAAQ;AACnD,SAAO,MAAM,WAAW,QAAQ;AAClC;AAOO,MAAM,eAAe,MAAiB;AAC3C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAoB,YAAY;AAC1D,YAAU,MAAM,kBAAkB,QAAQ,GAAG,CAAC,CAAC;AAC/C,SAAO;AACT;AAWO,MAAM,eAAe,OAAO,UAAoC;AACrE,QAAM,MAAO,MAAM,gBAAgB,SAAS,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAI9D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,qBAAqB;AAG3D,QAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,OAAO,WAAc;AAC5D,UAAM;AAAA,EACR;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/theme.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { protocolRequest } from './sandboxUtils';\n\n/**\n * The host UI theme, mirrored from the immediately.run host window into the\n * sandbox. Your app can read this to render in step with the host chrome\n * (light / dark).\n *\n * This is the baseline `theme:read` capability — every app may read it. Changing\n * the host theme is a separate, elevated action (`theme:set`), available only to\n * the theme-toggle system app.\n */\nexport type HostTheme = 'light' | 'dark';\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `theme` and\n// answers `request-theme` (wire format: site-main channelBridge.ts). The host's\n// default before it reports is `dark` (sandbox themeState.DEFAULT_THEME).\nconst channel = createPushChannel<HostTheme>({\n pushType: 'theme',\n requestType: 'request-theme',\n initial: 'dark',\n parse: (msg) => (msg.theme === 'light' || msg.theme === 'dark' ? msg.theme : undefined),\n});\n\n/**\n * Returns the current host theme. Poll this for a one-off read; use\n * {@link onHostThemeChange} or {@link useHostTheme} to react to changes.\n */\nexport const getHostTheme = (): HostTheme => channel.get();\n\n/**\n * Subscribe to host theme changes. The listener is invoked immediately with the\n * current theme, then again on every change. Returns an unsubscribe fn.\n */\nexport const onHostThemeChange = (listener: (theme: HostTheme) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current host theme, re-rendering when it changes.\n * The recommended way to implement an app's own `useTheme`: follow the host,\n * allow a local override.\n */\nexport const useHostTheme = (): HostTheme => channel.use();\n\n/**\n * Set the host UI theme — the ELEVATED `theme:set` action (§8.5). The host\n * applies it and re-pushes the new value to every `theme:read` iframe, so your\n * own {@link useHostTheme} confirms the change (the loop closes with no special\n * case). Only a grant holding `theme:set` (e.g. the theme-toggle system app) may\n * call this; any other app is rejected host-side with a `forbidden`\n * {@link Error} (carrying `.code`), regardless of what the app claims. Update\n * optimistically and let the re-push confirm.\n */\nexport const setHostTheme = async (theme: HostTheme): Promise<void> => {\n const res = (await protocolRequest('theme', 'set', [{ theme }])) as\n | { ok: true; data?: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'setHostTheme failed') as Error & {\n code?: string;\n };\n err.code = (res && 'code' in res ? res.code : undefined) ?? 'unknown';\n throw err;\n }\n};\n"],"mappings":"AAAA,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAgBhC,MAAM,UAAU,kBAA6B;AAAA,EAC3C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,IAAI,UAAU,WAAW,IAAI,UAAU,SAAS,IAAI,QAAQ;AAC/E,CAAC;AAMM,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAMlD,MAAM,oBAAoB,CAAC,aAChC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,eAAe,MAAiB,QAAQ,IAAI;AAWlD,MAAM,eAAe,OAAO,UAAoC;AACrE,QAAM,MAAO,MAAM,gBAAgB,SAAS,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;AAI9D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,qBAAqB;AAG3D,QAAI,QAAQ,OAAO,UAAU,MAAM,IAAI,OAAO,WAAc;AAC5D,UAAM;AAAA,EACR;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immediately-run/sdk",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "Runtime SDK for code executing inside an immediately.run sandbox.",
5
5
  "license": "MIT",
6
6
  "repository": "github:immediately-run/immediately-run-sdk",
@@ -27,8 +27,11 @@
27
27
  "scripts": {
28
28
  "build": "tsup",
29
29
  "test": "jest",
30
+ "check:circular": "node scripts/check-circular.mjs",
31
+ "api:check": "node scripts/check-api-stability.mjs",
32
+ "api:update": "node scripts/check-api-stability.mjs --update",
30
33
  "docs": "typedoc",
31
- "prepublishOnly": "npm run build"
34
+ "prepublishOnly": "npm run check:circular && npm run build && npm run api:check"
32
35
  },
33
36
  "peerDependencies": {
34
37
  "react": "^19.0.0",