@goflowstate/widget-sdk 0.1.1 → 0.2.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/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // @goflowstate/widget-sdk v0.1.1
1
+ // @goflowstate/widget-sdk v0.2.0
2
2
  // https://github.com/starteryllp/goflowstate-sdk
3
3
 
4
4
  var __defProp = Object.defineProperty;
@@ -39,10 +39,288 @@ function createServiceClient(opts) {
39
39
  };
40
40
  }
41
41
 
42
+ // src/session.ts
43
+ function readSessionLaunchParams() {
44
+ if (typeof window === "undefined") return null;
45
+ const q = new URLSearchParams(window.location.search);
46
+ const slug = q.get("slug") ?? q.get("tripId") ?? "";
47
+ if (!slug) return null;
48
+ return {
49
+ slug,
50
+ ticket: q.get("ticket"),
51
+ apiBase: (q.get("api") ?? "http://localhost:13002").replace(/\/$/, ""),
52
+ webBase: (q.get("web") ?? "http://localhost:14321").replace(/\/$/, "")
53
+ };
54
+ }
55
+ var SESSION_INIT_TIMEOUT_MS = 1e4;
56
+ var DEFAULT_POLL_INTERVAL_MS = 4e3;
57
+ function awaitInitTicket(webBase) {
58
+ return new Promise((resolve, reject) => {
59
+ if (typeof window === "undefined" || window.parent === window) {
60
+ reject(new Error("no parent page to receive a ticket from"));
61
+ return;
62
+ }
63
+ const webOrigin = new URL(webBase).origin;
64
+ const timer = setTimeout(() => {
65
+ window.removeEventListener("message", onMessage);
66
+ reject(new Error("timed out waiting for the session handoff"));
67
+ }, SESSION_INIT_TIMEOUT_MS);
68
+ const onMessage = (e) => {
69
+ if (e.origin !== webOrigin) return;
70
+ const data = e.data;
71
+ if (data?.type !== "flowstate:init" || typeof data.ticket !== "string") return;
72
+ clearTimeout(timer);
73
+ window.removeEventListener("message", onMessage);
74
+ resolve(data.ticket);
75
+ };
76
+ window.addEventListener("message", onMessage);
77
+ window.parent.postMessage({ type: "flowstate:ready" }, webOrigin);
78
+ });
79
+ }
80
+ var WIRE_PHASE = {
81
+ setup: "setup",
82
+ wishlist: "open",
83
+ sealed: "sealed",
84
+ plan: "resolved"
85
+ };
86
+ function toStateView(s) {
87
+ return {
88
+ slug: s.tripId,
89
+ phase: WIRE_PHASE[s.phase] ?? "open",
90
+ status: s.status,
91
+ setup: s.setup ?? null,
92
+ members: s.members,
93
+ sealedCount: s.sealedCount,
94
+ memberCount: s.memberCount,
95
+ allSealed: s.allSealed,
96
+ quorumMet: s.quorumMet ?? s.allSealed,
97
+ gate: s.gate ?? null,
98
+ flow: s.flow ?? null,
99
+ myContribution: s.myWishlist ?? null,
100
+ doc: s.plan ?? null,
101
+ artifactId: s.artifactId,
102
+ extension: s.extension ?? null
103
+ };
104
+ }
105
+ var parentOpener = (webBase) => {
106
+ return (url) => {
107
+ if (typeof window !== "undefined" && window.parent !== window) {
108
+ window.parent.postMessage({ type: "flowstate:navigate", url }, webBase);
109
+ return;
110
+ }
111
+ if (typeof window !== "undefined") window.open(url, "_blank", "noopener");
112
+ };
113
+ };
114
+ var HttpSessionClient = class {
115
+ constructor(params, opener) {
116
+ __publicField(this, "slug");
117
+ __publicField(this, "webBase");
118
+ __publicField(this, "apiBase");
119
+ __publicField(this, "tokenPromise");
120
+ __publicField(this, "opener");
121
+ this.slug = params.slug;
122
+ this.webBase = params.webBase;
123
+ this.apiBase = params.apiBase;
124
+ this.opener = opener ?? parentOpener(params.webBase);
125
+ this.tokenPromise = this.acquireTicket(params).then((t) => this.redeem(t));
126
+ }
127
+ async acquireTicket(params) {
128
+ if (params.ticket) return params.ticket;
129
+ return awaitInitTicket(params.webBase);
130
+ }
131
+ async redeem(ticket) {
132
+ const res = await fetch(`${this.apiBase}/api/auth/ticket/redeem`, {
133
+ method: "POST",
134
+ headers: { "content-type": "application/json" },
135
+ body: JSON.stringify({ ticket })
136
+ });
137
+ if (!res.ok) throw new Error(`ticket redeem failed (${res.status})`);
138
+ const json = await res.json();
139
+ return json.data.token;
140
+ }
141
+ async call(path, init = {}) {
142
+ const token = await this.tokenPromise;
143
+ const res = await fetch(`${this.apiBase}/api/w/${this.slug}${path}`, {
144
+ ...init,
145
+ headers: {
146
+ authorization: `Bearer ${token}`,
147
+ ...init.body ? { "content-type": "application/json" } : {},
148
+ ...init.headers ?? {}
149
+ }
150
+ });
151
+ if (!res.ok) {
152
+ const body = await res.json().catch(() => ({}));
153
+ throw new Error(body.message ?? `session service ${res.status}`);
154
+ }
155
+ const json = await res.json();
156
+ const data = json.data;
157
+ return toStateView(data.state ?? data);
158
+ }
159
+ join() {
160
+ return this.call("/enter", { method: "POST" });
161
+ }
162
+ state() {
163
+ return this.call("/state");
164
+ }
165
+ poll(onState, opts) {
166
+ const interval = opts?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
167
+ let stopped = false;
168
+ let timer = null;
169
+ const tick = async () => {
170
+ try {
171
+ const s = await this.state();
172
+ if (!stopped) onState(s);
173
+ } catch (err) {
174
+ opts?.onError?.(err);
175
+ }
176
+ if (!stopped) timer = setTimeout(tick, interval);
177
+ };
178
+ void tick();
179
+ return () => {
180
+ stopped = true;
181
+ if (timer) clearTimeout(timer);
182
+ };
183
+ }
184
+ commitSetup(setup) {
185
+ return this.call("/setup", { method: "POST", body: JSON.stringify({ setup }) });
186
+ }
187
+ contribute(payload) {
188
+ return this.call("/contribute", { method: "POST", body: JSON.stringify({ payload }) });
189
+ }
190
+ close() {
191
+ return this.call("/close", { method: "POST" });
192
+ }
193
+ action(verb, body) {
194
+ return this.call(`/${verb.replace(/^\//, "")}`, {
195
+ method: "POST",
196
+ body: JSON.stringify(body ?? {})
197
+ });
198
+ }
199
+ async request(verb, body) {
200
+ const token = await this.tokenPromise;
201
+ const res = await fetch(`${this.apiBase}/api/w/${this.slug}/${verb.replace(/^\//, "")}`, {
202
+ method: "POST",
203
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
204
+ body: JSON.stringify(body ?? {})
205
+ });
206
+ if (!res.ok) {
207
+ const errBody = await res.json().catch(() => ({}));
208
+ throw new Error(errBody.message ?? `session service ${res.status}`);
209
+ }
210
+ const json = await res.json();
211
+ return json.data;
212
+ }
213
+ openExternal(url) {
214
+ this.opener(url);
215
+ }
216
+ revealUrl(artifactId) {
217
+ return `${this.webBase}/a/${artifactId}`;
218
+ }
219
+ };
220
+ function createMockSession(opts = {}) {
221
+ const palette = ["#8b7ef8", "#f8a34d", "#4dc9a4", "#f26d8f", "#5aa9e6"];
222
+ const members = [
223
+ { userId: "mock-me", name: opts.me ?? "You", color: palette[0], sealed: false, isMe: true, isHost: true },
224
+ ...(opts.others ?? []).map((name, i) => ({
225
+ userId: `mock-${i + 1}`,
226
+ name,
227
+ color: palette[(i + 1) % palette.length],
228
+ sealed: false,
229
+ isMe: false,
230
+ isHost: false
231
+ }))
232
+ ];
233
+ let setup = opts.setup ?? null;
234
+ let myContribution = null;
235
+ let doc = null;
236
+ let status = "open";
237
+ const view = () => {
238
+ const sealedCount = members.filter((m) => m.sealed).length;
239
+ const allSealed = members.length > 0 && sealedCount >= members.length;
240
+ const phase = status === "closed" && doc ? "resolved" : !setup ? "setup" : allSealed ? "sealed" : "open";
241
+ return {
242
+ slug: "mock-session",
243
+ phase,
244
+ status,
245
+ setup,
246
+ members: members.map((m) => ({ ...m })),
247
+ sealedCount,
248
+ memberCount: members.length,
249
+ allSealed,
250
+ quorumMet: allSealed,
251
+ gate: null,
252
+ flow: null,
253
+ myContribution,
254
+ doc,
255
+ artifactId: doc ? "mock-artifact" : null,
256
+ extension: null
257
+ };
258
+ };
259
+ return {
260
+ slug: "mock-session",
261
+ webBase: "http://localhost:14321",
262
+ async join() {
263
+ return view();
264
+ },
265
+ async state() {
266
+ return view();
267
+ },
268
+ poll(onState, pollOpts) {
269
+ const interval = pollOpts?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
270
+ const timer = setInterval(() => onState(view()), interval);
271
+ onState(view());
272
+ return () => clearInterval(timer);
273
+ },
274
+ async commitSetup(s) {
275
+ setup = s;
276
+ return view();
277
+ },
278
+ async contribute(payload) {
279
+ myContribution = payload;
280
+ const me = members.find((m) => m.isMe);
281
+ if (me) me.sealed = true;
282
+ return view();
283
+ },
284
+ async close() {
285
+ status = "closed";
286
+ doc = opts.synthesize ? opts.synthesize([myContribution]) : { mock: true };
287
+ return view();
288
+ },
289
+ async action() {
290
+ return view();
291
+ },
292
+ async request() {
293
+ return {};
294
+ },
295
+ openExternal(url) {
296
+ if (typeof window !== "undefined") window.open(url, "_blank", "noopener");
297
+ },
298
+ revealUrl(artifactId) {
299
+ return `http://localhost:14321/a/${artifactId}`;
300
+ }
301
+ };
302
+ }
303
+ function initSession() {
304
+ const params = readSessionLaunchParams();
305
+ if (!params) return null;
306
+ return new HttpSessionClient(params);
307
+ }
308
+
42
309
  // src/sdk.ts
43
310
  var PROTOCOL = "canvas-widget-v1";
44
311
  var DEV_MODE_TOKEN = "dev-mode-skip";
45
312
  var INIT_TIMEOUT_MS = 5e3;
313
+ function safeRandomUUID() {
314
+ const c = globalThis.crypto;
315
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
316
+ const b = new Uint8Array(16);
317
+ if (c && typeof c.getRandomValues === "function") c.getRandomValues(b);
318
+ else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
319
+ b[6] = b[6] & 15 | 64;
320
+ b[8] = b[8] & 63 | 128;
321
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, "0"));
322
+ return `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}`;
323
+ }
46
324
  var cachedKey = null;
47
325
  async function getHmacKey(secret) {
48
326
  if (cachedKey && cachedKey.secret === secret) return cachedKey.key;
@@ -69,6 +347,7 @@ var CanvasWidgetSDKImpl = class {
69
347
  __publicField(this, "_state");
70
348
  __publicField(this, "_hmacSecret");
71
349
  __publicField(this, "_user");
350
+ __publicField(this, "_namespace");
72
351
  __publicField(this, "_widgetApiUrl");
73
352
  __publicField(this, "_widgetApiToken");
74
353
  __publicField(this, "_installationId");
@@ -89,6 +368,7 @@ var CanvasWidgetSDKImpl = class {
89
368
  this._state = init.state;
90
369
  this._hmacSecret = init.hmacSecret;
91
370
  this._user = init.user;
371
+ this._namespace = init.namespace ?? null;
92
372
  this._widgetApiUrl = init.widgetApiUrl;
93
373
  this._widgetApiToken = init.widgetApiToken ?? null;
94
374
  this._installationId = init.installationId;
@@ -114,6 +394,11 @@ var CanvasWidgetSDKImpl = class {
114
394
  get user() {
115
395
  return this._user;
116
396
  }
397
+ /** Namespace (office) context — id and owner-inclusive member count — or
398
+ * null on a personal canvas. What the `canvas.context` permission grants. */
399
+ get namespace() {
400
+ return this._namespace;
401
+ }
117
402
  /** Subscribe to display state changes; returns an unsubscribe function. */
118
403
  onStateChange(cb) {
119
404
  this._stateListeners.push(cb);
@@ -377,7 +662,7 @@ var CanvasWidgetSDKImpl = class {
377
662
  });
378
663
  }
379
664
  async buildMessage(type, payload) {
380
- const nonce = crypto.randomUUID();
665
+ const nonce = safeRandomUUID();
381
666
  const timestamp = Date.now();
382
667
  let token;
383
668
  if (this._hmacSecret) {
@@ -407,6 +692,13 @@ var CanvasWidgetSDKImpl = class {
407
692
  }
408
693
  };
409
694
  var CanvasWidget = {
695
+ /**
696
+ * Initialize a group-session client for widgets hosted on the /w/[slug]
697
+ * session page (ticket handshake + session REST rails). Returns null when
698
+ * the launch context carries no session params — see `initSession` in
699
+ * ./session for details and `createMockSession` for standalone dev.
700
+ */
701
+ initSession,
410
702
  /**
411
703
  * Initialize the SDK and establish communication with the canvas host.
412
704
  *
@@ -436,7 +728,7 @@ var CanvasWidget = {
436
728
  type: "READY",
437
729
  widgetId: "",
438
730
  // Not yet known — host identifies us by iframe source
439
- nonce: crypto.randomUUID(),
731
+ nonce: safeRandomUUID(),
440
732
  timestamp: Date.now(),
441
733
  token: DEV_MODE_TOKEN,
442
734
  // READY is always allowed, token doesn't matter
@@ -446,8 +738,84 @@ var CanvasWidget = {
446
738
  });
447
739
  }
448
740
  };
741
+
742
+ // src/theme.ts
743
+ var THEME_MESSAGE_TYPE = "flowstate:theme";
744
+ var DEFAULT_THEME = "glass";
745
+ function isWidgetTheme(value) {
746
+ return value === "light" || value === "dark" || value === "glass";
747
+ }
748
+ function getInitialTheme() {
749
+ if (typeof window === "undefined") return DEFAULT_THEME;
750
+ try {
751
+ const param = new URLSearchParams(window.location.search).get("theme");
752
+ return isWidgetTheme(param) ? param : DEFAULT_THEME;
753
+ } catch {
754
+ return DEFAULT_THEME;
755
+ }
756
+ }
757
+ function onThemeChange(cb, opts) {
758
+ if (typeof window === "undefined") return () => {
759
+ };
760
+ const listener = (event) => {
761
+ if (event.source !== window.parent) return;
762
+ if (opts?.allowedOrigin && event.origin !== opts.allowedOrigin) return;
763
+ const data = event.data;
764
+ if (typeof data !== "object" || data === null) return;
765
+ const message = data;
766
+ if (message.type !== THEME_MESSAGE_TYPE) return;
767
+ if (!isWidgetTheme(message.theme)) return;
768
+ cb(message.theme);
769
+ };
770
+ window.addEventListener("message", listener);
771
+ return () => window.removeEventListener("message", listener);
772
+ }
773
+ function applyWidgetTheme(theme) {
774
+ if (typeof document === "undefined") return;
775
+ if (theme === "glass") {
776
+ delete document.documentElement.dataset.theme;
777
+ } else {
778
+ document.documentElement.dataset.theme = theme;
779
+ }
780
+ }
781
+ function initWidgetTheme() {
782
+ applyWidgetTheme(getInitialTheme());
783
+ return onThemeChange(applyWidgetTheme);
784
+ }
785
+
786
+ // src/ingest.ts
787
+ var INGEST_MESSAGE_TYPE = "flowstate:ingest";
788
+ function onArtifactIngested(cb, opts) {
789
+ if (typeof window === "undefined") return () => {
790
+ };
791
+ const listener = (event) => {
792
+ if (event.source !== window.parent) return;
793
+ if (opts?.allowedOrigin && event.origin !== opts.allowedOrigin) return;
794
+ const data = event.data;
795
+ if (typeof data !== "object" || data === null) return;
796
+ const message = data;
797
+ if (message.type !== INGEST_MESSAGE_TYPE) return;
798
+ if (typeof message.artifactId !== "string" || !message.artifactId) return;
799
+ cb({
800
+ artifactId: message.artifactId,
801
+ kind: typeof message.kind === "string" ? message.kind : void 0,
802
+ payload: message.payload
803
+ });
804
+ };
805
+ window.addEventListener("message", listener);
806
+ return () => window.removeEventListener("message", listener);
807
+ }
449
808
  export {
450
809
  CanvasWidget,
451
- createServiceClient
810
+ INGEST_MESSAGE_TYPE,
811
+ applyWidgetTheme,
812
+ createMockSession,
813
+ createServiceClient,
814
+ getInitialTheme,
815
+ initSession,
816
+ initWidgetTheme,
817
+ onArtifactIngested,
818
+ onThemeChange,
819
+ readSessionLaunchParams
452
820
  };
453
821
  //# sourceMappingURL=index.mjs.map