@wakeshift/sdk 0.2.0 → 0.4.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/wakeshift.js CHANGED
@@ -1,7 +1,6 @@
1
- import { z } from "zod";
2
1
  import { WakeshiftError } from "./errors.js";
3
2
  import { WakeEvent, attachEvents } from "./events.js";
4
- import { parseKey, signAssertion } from "./key.js";
3
+ import { parseKey, signAssertion, isTestKey } from "./key.js";
5
4
  import { retryRefused } from "./retry.js";
6
5
  /** Wakeshift's production core, the default for every key but an older wst_test_ one. Override with `core` or WAKESHIFT_CORE. */
7
6
  export const DEFAULT_CORE = "https://app.wakeshift.ai";
@@ -14,19 +13,25 @@ export const HARNESS_ARG = "harness";
14
13
  * when its program gives it one (Cursor and Grok Bot: CURSOR_CONVERSATION_ID).
15
14
  */
16
15
  export const CONVERSATION_ARG = "conversation_id";
17
- // What the agent is told each argument is (the core's own words:
18
- // internal/harnesses, whose tests check these match).
19
- const SESSION_DESCRIPTION = "The session key an earlier result gave you, if it did. It's a secret: keep it to this conversation; don't share or log it";
20
- const HARNESS_DESCRIPTION = "The program you're running in: codex, claude-code, cursor (local CLI or IDE), cursor-cloud (Cursor cloud agent), grok-bot, copilot-cli, opencode, antigravity, openclaw (even when it runs your turns in Codex), hermes, chatgpt, or your program's name if it isn't listed";
21
- const CONVERSATION_DESCRIPTION = "Your own ID for this conversation, if your program gives you one. Cursor and Grok Bot: the output of `printenv CURSOR_CONVERSATION_ID`. Codex and Antigravity send it automatically; others leave it empty";
22
- /** What the SDK says, once, when Wakeshift recognised no harness in a tool call. */
23
- export const UNKNOWN_CLIENT_WARNING = "wakeshift: Wakeshift couldn't tell which agent made a tool call (no MCP client name, nothing it knows in the " +
24
- "call's _meta or User-Agent, and no harness argument), so it treats the agent as one it can't wake (its messages are held " +
25
- "for its next call to the tool). The name comes from the client's initialize request, which a server created per request " +
26
- "(stateless Streamable HTTP) never sees; Claude Code, Codex, Antigravity, Copilot CLI, opencode and Cursor are recognised " +
27
- "without it. Spread ...wake.args into the tool's input and pass args to wake.call, so the agent can say (harness); for " +
28
- "other clients, keep one McpServer per MCP session (a stateful transport), or pass the name yourself: " +
29
- "wake.call(extra, { client }) or wake.callOf({ clientName }).";
16
+ // recipientsOf reads an emit's unmatched or ended list: "user",
17
+ // { native: user } or { user, subscription }.
18
+ function recipientsOf(raw) {
19
+ if (!Array.isArray(raw))
20
+ return [];
21
+ const out = [];
22
+ for (const r of raw) {
23
+ if (typeof r === "string")
24
+ out.push(r);
25
+ else if (r && typeof r === "object") {
26
+ const m = r;
27
+ if (typeof m.native === "string" && m.native)
28
+ out.push({ native: m.native });
29
+ else if (typeof m.user === "string")
30
+ out.push({ user: m.user, subscription: typeof m.subscription === "string" ? m.subscription : "" });
31
+ }
32
+ }
33
+ return out;
34
+ }
30
35
  // userAgentOf is the User-Agent of the HTTP request a tool call came in
31
36
  // ("" if there's none, e.g. over stdio).
32
37
  function userAgentOf(extra) {
@@ -36,45 +41,51 @@ function userAgentOf(extra) {
36
41
  const v = headers["user-agent"] ?? headers["User-Agent"];
37
42
  return (Array.isArray(v) ? v[0] : v) ?? "";
38
43
  }
39
- export function clientNameOf(s) {
40
- const info = "getClientVersion" in s ? s.getClientVersion() : s.server.getClientVersion();
41
- return info?.name ?? "";
42
- }
43
- /** Adds Wakeshift's text and _meta to a tool result (a copy). */
44
- export function applyAddition(a, base) {
45
- let out;
46
- if (typeof base === "string") {
47
- const text = (base.trim() + "\n\n" + (a.text ?? "")).trim();
48
- out = { content: text ? [{ type: "text", text }] : [] };
49
- }
50
- else {
51
- out = { ...base, content: [...(base.content ?? [])] };
52
- if (a.text)
53
- out.content.push({ type: "text", text: a.text });
54
- }
55
- if (a.meta && Object.keys(a.meta).length > 0)
56
- out._meta = { ...(out._meta ?? {}), ...a.meta };
44
+ /** @internal Packs a tool call as Wakeshift takes it: empty fields left out, the agent's claims trimmed. */
45
+ export function packCall(c) {
46
+ const out = { client: c.clientName ?? "" };
47
+ if (c.user)
48
+ out.user = c.user;
49
+ if (c.meta && Object.keys(c.meta).length > 0)
50
+ out.meta = c.meta;
51
+ if (c.session)
52
+ out.session = c.session;
53
+ if (c.userAgent)
54
+ out.user_agent = c.userAgent;
55
+ if (c.harness?.trim())
56
+ out.harness = c.harness.trim();
57
+ if (c.conversation?.trim())
58
+ out.conversation = c.conversation.trim();
59
+ if (c.tool?.trim())
60
+ out.tool = c.tool.trim();
57
61
  return out;
58
62
  }
59
- const argsShape = {
60
- [SESSION_ARG]: z.string().optional().describe(SESSION_DESCRIPTION),
61
- [HARNESS_ARG]: z.string().optional().describe(HARNESS_DESCRIPTION),
62
- [CONVERSATION_ARG]: z.string().optional().describe(CONVERSATION_DESCRIPTION),
63
- };
63
+ /**
64
+ * @internal The call to one of attach's own tools (subscribe_events and the
65
+ * others): its _meta and User-Agent, the client's name, the user, and
66
+ * session_key, harness and conversation_id from its arguments.
67
+ */
68
+ export function toolCallOf(extra, client, user, args, tool) {
69
+ return packCall({
70
+ meta: extra?._meta,
71
+ clientName: client,
72
+ user,
73
+ session: str(args, SESSION_ARG),
74
+ userAgent: userAgentOf(extra),
75
+ harness: str(args, HARNESS_ARG),
76
+ conversation: str(args, CONVERSATION_ARG),
77
+ tool,
78
+ });
79
+ }
64
80
  // str is a tool argument's value if it's a string.
65
81
  function str(args, name) {
66
82
  return typeof args?.[name] === "string" ? args[name] : undefined;
67
83
  }
68
84
  /**
69
- * The Wakeshift client for an app: subscribe the conversations that call
70
- * your tools to topics, and publish to them.
85
+ * The Wakeshift client for an app: define the events your app offers,
86
+ * attach them to your MCP server, and emit them when they happen.
71
87
  */
72
88
  export class Wakeshift {
73
- /**
74
- * An older key's mode (wst_test_…, wst_live_…); null for a current key.
75
- * @deprecated Modes mean nothing now, and current keys have none.
76
- */
77
- mode;
78
89
  core;
79
90
  key;
80
91
  // The app's ID: from the key, if it names the app, or else from app().
@@ -89,10 +100,9 @@ export class Wakeshift {
89
100
  throw new Error("wakeshift: no app key (WAKESHIFT_KEY?)");
90
101
  this.key = parseKey(opts.key);
91
102
  this.knownAppId = this.key.appId;
92
- this.mode = this.key.mode;
93
103
  // Test keys come from staging and local consoles, and one sent to
94
104
  // production fails in a way that's hard to read: ask for the core.
95
- const core = opts.core || process.env.WAKESHIFT_CORE || (this.mode === "test" ? "" : DEFAULT_CORE);
105
+ const core = opts.core || process.env.WAKESHIFT_CORE || (isTestKey(opts.key) ? "" : DEFAULT_CORE);
96
106
  if (!core) {
97
107
  throw new Error("wakeshift: no core URL for this test key: set WAKESHIFT_CORE, or pass { core }, to the address of the console the key came from (for example https://app.staging.wakeshift.ai)");
98
108
  }
@@ -113,7 +123,8 @@ export class Wakeshift {
113
123
  return this.knownAppId;
114
124
  }
115
125
  /**
116
- * Which app the key is, as Wakeshift says: its ID and name. Asks once
126
+ * Which app the key is, as Wakeshift says: its ID and name, and the
127
+ * key's. Asks once
117
128
  * (GET /api/apps/v1/app) and remembers the answer; afterwards
118
129
  * `wake.appId` works for any key.
119
130
  */
@@ -125,7 +136,8 @@ export class Wakeshift {
125
136
  }
126
137
  const id = this.knownAppId ?? r.id;
127
138
  this.knownAppId = id;
128
- return { id, name: typeof r.name === "string" ? r.name : "" };
139
+ const str = (v) => (typeof v === "string" ? v : "");
140
+ return { id, name: str(r.name), key: { id: str(r.key?.key_id), name: str(r.key?.name) } };
129
141
  })
130
142
  .catch((err) => {
131
143
  this.appInfo = undefined; // a later call asks again
@@ -145,202 +157,103 @@ export class Wakeshift {
145
157
  warn(message) {
146
158
  this.warnFn(message);
147
159
  }
148
- /** Reports a warning, once per kind. */
160
+ /** @internal Reports a warning, once per kind. */
149
161
  warnOnce(kind, message) {
150
162
  if (this.warned.has(kind))
151
163
  return;
152
164
  this.warned.add(kind);
153
165
  this.warnFn(message);
154
166
  }
155
- /**
156
- * The arguments the agent passes Wakeshift's part of the call in, as a
157
- * zod shape for your subscribe tool's input (and any other tool whose
158
- * results you annotate): `inputSchema: { repo: z.string(), ...wake.args }`.
159
- * session_key (the key a result gives the conversation), harness (the
160
- * program the agent runs in) and conversation_id (its own ID for the
161
- * conversation, where its program gives one). Wakeshift adds nothing to
162
- * tools you don't spread it into.
163
- */
164
- get args() {
165
- return argsShape;
166
- }
167
- /**
168
- * Packs the tool call from an MCP tool callback's `extra`: its _meta and
169
- * User-Agent, the client's name (from `server`, or pass `client`), the
170
- * user, and session_key, harness and conversation_id from `args`.
171
- */
172
- call(extra, opts = {}) {
173
- let client = opts.client;
174
- if (client === undefined) {
175
- if (!opts.server)
176
- throw new Error("wakeshift: wake.call needs the MCP client's name: pass { server } (your McpServer), or { client }");
177
- client = clientNameOf(opts.server);
178
- }
179
- const args = opts.args;
180
- return this.callOf({
181
- meta: extra?._meta,
182
- clientName: client,
183
- user: opts.user,
184
- session: opts.session ?? str(args, SESSION_ARG),
185
- userAgent: userAgentOf(extra),
186
- harness: opts.harness ?? str(args, HARNESS_ARG),
187
- conversation: opts.conversation ?? str(args, CONVERSATION_ARG),
188
- });
189
- }
190
- /** Packs a tool call without an MCP framework. */
191
- callOf(c) {
192
- const out = { client: c.clientName ?? "" };
193
- if (c.user)
194
- out.user = c.user;
195
- if (c.meta && Object.keys(c.meta).length > 0)
196
- out.meta = c.meta;
197
- if (c.session)
198
- out.session = c.session;
199
- if (c.userAgent)
200
- out.user_agent = c.userAgent;
201
- if (c.harness?.trim())
202
- out.harness = c.harness.trim();
203
- if (c.conversation?.trim())
204
- out.conversation = c.conversation.trim();
205
- return out;
206
- }
207
- // recognised reads what the core recognised the caller as, and warns
208
- // once if it was nothing and the agent didn't say (an older core that
209
- // doesn't say: no warning).
210
- recognised(agent) {
211
- if (agent && !agent.harness && !agent.claimed)
212
- this.warnOnce("unknown_client", UNKNOWN_CLIENT_WARNING);
213
- return { harness: agent?.harness ?? null, via: agent?.via ?? null, claimed: agent?.claimed ?? null };
214
- }
215
- /**
216
- * Subscribes the calling conversation to topic (again: refreshes it).
217
- * Check the caller may see the topic first: Wakeshift only refuses another
218
- * user's u/<user>/… topics.
219
- */
220
- async subscribe(call, topic, opts = {}) {
221
- const body = { call, topic, once: !!opts.once };
222
- if (opts.filter && Object.keys(opts.filter).length > 0)
223
- body.filter = opts.filter;
224
- if (opts.replay !== undefined)
225
- body.replay = opts.replay;
226
- if (opts.ttl !== undefined)
227
- body.ttl = opts.ttl;
228
- const out = await this.request("POST", "/topics/subscribe", body);
229
- const agent = this.recognised(out.agent);
230
- const addition = out.addition ?? {};
231
- const s = out.subscription;
232
- return {
233
- id: s.id,
234
- topic: s.topic,
235
- filter: s.filter,
236
- once: s.once,
237
- expiresAt: new Date(s.expires_at),
238
- session: out.session,
239
- addition,
240
- agent,
241
- result: (text) => applyAddition(addition, text),
242
- };
243
- }
244
- /** Ends the conversation's subscription to topic, or to all the app's topics. Returns how many ended. */
245
- async unsubscribe(call, topic) {
246
- const out = await this.request("POST", "/topics/unsubscribe", topic ? { call, topic } : { call });
247
- return out.unsubscribed;
248
- }
249
- /** Wakes every conversation subscribed to one of topics whose filter the event passes (each once). */
250
- async publish(topics, event) {
251
- const out = await this.request("POST", "/topics/publish", { topics: typeof topics === "string" ? [topics] : topics, event });
252
- return { matched: out.matched, duplicate: !!out.duplicate };
253
- }
254
- /** Wakes exactly one subscription's conversation, whatever its filter. */
255
- async send(subscriptionId, event) {
256
- const out = await this.request("POST", `/subscriptions/${encodeURIComponent(subscriptionId)}/send`, { event });
257
- return out.status;
258
- }
259
- /**
260
- * How many conversations are subscribed to topic, and how many are
261
- * connected now, and how. Only reads: nothing is delivered, minted or ended.
262
- */
263
- async topicStatus(topic) {
264
- const out = await this.request("GET", `/topics/status?topic=${encodeURIComponent(topic)}`);
265
- return { topic: out.topic, subscriptions: out.subscriptions, connected: out.connected, via: { stream: 0, poll: 0, hooks: 0, webhook: 0, ...out.via } };
266
- }
267
- /** Whether a subscription is active and its conversation connected now, and how. Only reads. */
268
- async subscriptionStatus(subscriptionId) {
269
- const out = await this.request("GET", `/subscriptions/${encodeURIComponent(subscriptionId)}`);
270
- const s = { id: out.id, topic: out.topic, status: out.status, connected: out.connected };
271
- if (out.via)
272
- s.via = out.via;
273
- if (out.expires_at)
274
- s.expiresAt = new Date(out.expires_at);
275
- return s;
276
- }
277
- /** A capability URL that publishes to topic: a POST's body becomes an event. */
278
- async publishUrl(topic) {
279
- return this.request("POST", "/topics/publish-urls", { topic });
280
- }
281
- /** Stops a publish URL working, a topic's or an event's (410 from then on). */
167
+ /** Stops a publish URL working (410 from then on). */
282
168
  async revokePublishUrl(id) {
283
169
  await this.request("DELETE", `/events/publish-urls/${encodeURIComponent(id)}`);
284
170
  }
285
- /** What Wakeshift would add to any other tool result for this conversation (creates nothing). */
286
- async addition(call) {
287
- const out = await this.request("POST", "/topics/annotate", { call });
288
- return { ...out, agent: this.recognised(out.agent) };
289
- }
290
- /**
291
- * Adds what Wakeshift has for the calling conversation to a tool result
292
- * of yours (a copy): what's waiting for a harness that can't be woken, a
293
- * fresh connector beacon while a Codex thread's connector isn't running.
294
- * Only results you pass here (and a subscription's result()) change.
295
- */
296
- async annotate(call, result) {
297
- return applyAddition(await this.addition(call), result);
298
- }
299
- /** Ends every subscription to topic; each conversation is told. Returns how many ended. */
300
- async closeTopic(topic) {
301
- return (await this.request("POST", "/topics/close", { topic })).closed;
302
- }
303
- /**
304
- * Ends one subscription by its id, telling its conversation that the app
305
- * ended it. Returns false if it had already ended (or there's no such
306
- * subscription of the app's).
307
- */
308
- async endSubscription(subscriptionId) {
309
- return (await this.request("POST", `/subscriptions/${encodeURIComponent(subscriptionId)}/end`, {})).ended;
310
- }
311
- /**
312
- * Ends the subscriptions of every conversation of one of the app's users
313
- * to topic, or to all of the app's topics (they lost access to something),
314
- * telling each conversation. Returns how many ended.
315
- */
316
- async unsubscribeUser(user, topic) {
317
- return (await this.request("POST", "/topics/unsubscribe", topic ? { user, topic } : { user })).unsubscribed;
318
- }
319
171
  /**
320
172
  * Deletes everything Wakeshift keeps for one of the app's users: their
321
- * conversations, subscriptions, events and deliveries, recent publishes to
322
- * their u/<user>/… topics and publish URLs for them, and their ID. Open
323
- * streams close. Returns false if there was nothing to forget.
173
+ * conversations, subscriptions, events and deliveries, publish URLs for
174
+ * them, and their ID. Open streams close. Returns false if there was nothing to forget.
324
175
  */
325
176
  async forgetUser(user) {
326
177
  return (await this.request("POST", "/users/forget", { user })).forgotten;
327
178
  }
328
179
  /**
329
- * Tells Wakeshift what the app may do, replacing what it said before.
330
- * The events you defined (defineEvent) are added unless m has events of
331
- * its own. Returns the manifest as Wakeshift keeps it.
180
+ * Tells Wakeshift what the app may do, replacing what it said before: m,
181
+ * with the events you defined (defineEvent) after m's own events. An
182
+ * event both defined and listed in m.events is an error, and nothing is
183
+ * sent. You needn't call it: Wakeshift gets the manifest automatically
184
+ * when the app first needs it. Once called, m is what every automatic
185
+ * send carries (even if this one failed), with the events defined by
186
+ * then. Returns the manifest as Wakeshift keeps it.
332
187
  */
333
188
  async publishManifest(m) {
334
- const body = m.events === undefined && this.events.size > 0 ? { ...m, events: [...this.events.values()].map((e) => e.manifest) } : m;
335
- const out = await this.request("PUT", "/manifest", body);
189
+ const version = this.manifestVersion; // before the body: a later defineEvent is sent again
190
+ const body = this.manifestBody(m);
336
191
  this.manifestBase = m;
337
- this.published = Promise.resolve();
338
- return out;
192
+ const run = this.serial(() => this.request("PUT", "/manifest", body));
193
+ this.track(run, version);
194
+ return run;
195
+ }
196
+ // serial runs a send after every send before it, explicit or automatic,
197
+ // so the last manifest Wakeshift gets is the newest.
198
+ sending = Promise.resolve();
199
+ serial(send) {
200
+ const run = this.sending.then(send, send);
201
+ this.sending = run.catch(() => undefined);
202
+ return run;
203
+ }
204
+ // track makes a send the one event calls wait for: once it's done,
205
+ // Wakeshift has the manifest for the events defined at version, unless it
206
+ // failed (the next call sends again) or an event was defined since.
207
+ track(send, version) {
208
+ const p = send.then(() => {
209
+ if (this.manifestVersion !== version && this.published === p)
210
+ this.published = null;
211
+ });
212
+ this.published = p;
213
+ p.catch(() => {
214
+ if (this.published === p)
215
+ this.published = null;
216
+ });
217
+ }
218
+ // The manifest as Wakeshift gets it: m's events, then the defined ones,
219
+ // refusing a name in both.
220
+ manifestBody(m) {
221
+ const listed = m.events ?? [];
222
+ for (const name of this.events.keys()) {
223
+ if (listed.some((e) => e.name === name)) {
224
+ throw new Error(`wakeshift: event ${name} is defined with defineEvent and listed in the manifest's events: keep one`);
225
+ }
226
+ }
227
+ const events = [...listed, ...[...this.events.values()].map((e) => e.manifest)];
228
+ const { events: _listed, ...rest } = m;
229
+ return events.length > 0 ? { ...rest, events } : rest;
230
+ }
231
+ // The automatic send: the manifest the app last published, or else the
232
+ // settings Wakeshift has (asked for first, so the send never wipes what
233
+ // was published separately), with the defined events.
234
+ async sendManifest() {
235
+ if (this.manifestBase) {
236
+ await this.request("PUT", "/manifest", this.manifestBody(this.manifestBase));
237
+ return;
238
+ }
239
+ const stored = await this.request("GET", "/manifest");
240
+ const kept = { description: typeof stored.description === "string" ? stored.description : "" };
241
+ if (stored.agent !== null && typeof stored.agent === "object")
242
+ kept.agent = stored.agent;
243
+ if (stored.others_can_wake === true)
244
+ kept.others_can_wake = true;
245
+ if (Array.isArray(stored.preapprove_tools) && stored.preapprove_tools.length > 0)
246
+ kept.preapprove_tools = stored.preapprove_tools;
247
+ await this.request("PUT", "/manifest", this.manifestBody(kept));
339
248
  }
340
249
  // ── Events (MCP Events, natively or bridged) ──────────────────────────
341
250
  events = new Map();
342
- manifestBase = {};
251
+ // What publishManifest was last given (null: nothing yet); whether
252
+ // Wakeshift has the manifest for the events defined now (the send; null:
253
+ // not yet, or a defineEvent since); and how many defineEvents there were.
254
+ manifestBase = null;
343
255
  published = null;
256
+ manifestVersion = 0;
344
257
  /**
345
258
  * Defines one of the app's event types: what a subscriber picks (args),
346
259
  * what each occurrence carries (payload), and the line an agent reads
@@ -360,7 +273,8 @@ export class Wakeshift {
360
273
  throw new Error(`wakeshift: event ${name} is defined twice`);
361
274
  const ev = new WakeEvent(this, name, opts);
362
275
  this.events.set(name, ev);
363
- this.published = null; // the manifest changed
276
+ this.published = null; // the manifest changed: sent again when next needed
277
+ this.manifestVersion++;
364
278
  return ev;
365
279
  }
366
280
  /**
@@ -372,21 +286,19 @@ export class Wakeshift {
372
286
  * unsubscribe_events and list_subscriptions for every client, so a person
373
287
  * can always see and stop what they're subscribed to.
374
288
  *
375
- * wake.attach(server, { identify: (extra) => ({ user: guestOf(extra)?.user }) })
289
+ * const attached = wake.attach(server, { identify: (extra) => ({ user: myUser(extra) }) })
290
+ *
291
+ * It returns the handle your own tools use to subscribe the conversation
292
+ * calling them: wrap a tool's callback with attached.events, and give it
293
+ * Wakeshift's arguments with attached.inputs.
376
294
  */
377
295
  attach(server, opts = {}) {
378
- attachEvents(this, server, [...this.events.values()], opts);
296
+ return attachEvents(this, server, [...this.events.values()], opts);
379
297
  }
380
- /** @internal Publishes the manifest with the defined events, once (again after a defineEvent). */
298
+ /** @internal Sends the manifest with the defined events, once (again after a defineEvent). */
381
299
  ensureEvents() {
382
- if (!this.published) {
383
- const p = this.publishManifest(this.manifestBase).then(() => undefined);
384
- this.published = p;
385
- p.catch(() => {
386
- if (this.published === p)
387
- this.published = null; // try again next time
388
- });
389
- }
300
+ if (!this.published)
301
+ this.track(this.serial(() => this.sendManifest()), this.manifestVersion);
390
302
  return this.published;
391
303
  }
392
304
  /** @internal An event's emit. */
@@ -398,19 +310,18 @@ export class Wakeshift {
398
310
  if (e.to)
399
311
  body.to = e.to;
400
312
  const out = await this.request("POST", "/events/emit", body);
401
- return { matched: out.matched, duplicate: !!out.duplicate };
313
+ return { matched: out.matched, duplicate: !!out.duplicate, unmatched: recipientsOf(out.unmatched), ended: recipientsOf(out.ended) };
402
314
  }
403
315
  /** @internal A call to the app API (the events machinery's), after the manifest with the defined events is published. */
404
316
  async requestApp(method, path, body) {
405
317
  await this.ensureEvents();
406
318
  return this.request(method, path, body);
407
319
  }
408
- // ── Guests' workspaces (GuestAuth) ─────────────────────────────────────
320
+ // ── Users' workspaces ──────────────────────────────────────────────────
409
321
  /**
410
322
  * The workspace Wakeshift keeps for one of the app's users (user: the
411
- * app's own ID for them, as in wake.call's user), creating it on first use.
412
- * Topics keep that user's conversations there too. GuestAuth calls this;
413
- * most apps never need to.
323
+ * app's own ID for them, as attach's identify gives it), created on first
324
+ * use. Wakeshift keeps that user's conversations there.
414
325
  */
415
326
  async workspace(user, name) {
416
327
  return (await this.request("POST", "/workspaces", { external_id: user, name })).workspace;
@@ -427,10 +338,6 @@ export class Wakeshift {
427
338
  async checkAgent(workspace, agent) {
428
339
  await this.request("POST", `/workspaces/${encodeURIComponent(workspace)}/agents/${encodeURIComponent(agent)}/token`);
429
340
  }
430
- /** @internal The app key's seed: what GuestAuth keys its signed values from. Never send or log it. */
431
- secretSeed() {
432
- return this.key.seed;
433
- }
434
341
  async request(method, path, body) {
435
342
  const headers = { Authorization: "Bearer " + this.assertion() };
436
343
  if (body !== undefined)
@@ -457,13 +364,16 @@ export class Wakeshift {
457
364
  json = undefined;
458
365
  }
459
366
  if (!resp.ok) {
460
- const e = json?.error ?? {};
461
- throw new WakeshiftError(resp.status, e.code ?? "", e.message ?? (text.slice(0, 200) || resp.statusText), {
462
- reason: typeof e.reason === "string" ? e.reason : undefined,
367
+ // Wakeshift's code, message and reason where it sent them as strings, else
368
+ // the body's start, else the status's text, as every SDK reads it (test/errors.json).
369
+ const e = json?.error !== null && typeof json?.error === "object" ? json.error : {};
370
+ const str = (v) => (typeof v === "string" ? v : "");
371
+ throw new WakeshiftError(resp.status, str(e.code), str(e.message) || text.slice(0, 200) || resp.statusText, {
372
+ reason: str(e.reason) || undefined,
463
373
  });
464
374
  }
465
375
  // A 2xx that isn't Wakeshift's JSON: a proxy's page, or a wrong core.
466
- if (json === null || typeof json !== "object") {
376
+ if (json === null || typeof json !== "object" || Array.isArray(json)) {
467
377
  throw new WakeshiftError(resp.status, "bad_response", `Wakeshift at ${this.core} answered ${resp.status} without a JSON object. Check that WAKESHIFT_CORE, or the core option, is the address of the console your key came from`);
468
378
  }
469
379
  return json;