@devicechain/client 0.17.0 → 0.18.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/README.md CHANGED
@@ -97,6 +97,12 @@ One socket is shared per area and reused across subscriptions. `connected` and
97
97
  `closed` are connection-level signals, which is what lets a UI tell "connected
98
98
  but idle" apart from "offline" — a distinction a data-only sink cannot make.
99
99
 
100
+ The socket carries subscriptions only; send queries and mutations over HTTP. The
101
+ server closes it with code `4401` when the access token it opened with expires.
102
+ `subscribe` handles that close for you: it reconnects once with a freshly resolved
103
+ token, subscribes again, and reports the reconnect as `connected(true)`, so re-query
104
+ anything you must not miss when you see it.
105
+
100
106
  ## Also in here
101
107
 
102
108
  - **Basemap resolution** (`resolveBasemap`, `renderableBasemap`) — the client half
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { gql, areaPath, areaKey, AREAS, resolveAuthToken, setAuthTokenGetter, setIdentityTokenGetter, GraphQLRequestError, type Area, type RequestOptions, type TypedDocument, } from './transport';
1
+ export { gql, areaPath, areaKey, AREAS, resolveAuthToken, setAuthTokenGetter, setIdentityTokenGetter, GraphQLRequestError, type GraphQLErrorEntry, type Area, type RequestOptions, type TypedDocument, } from './transport';
2
2
  export { isForbiddenError } from './errors';
3
3
  export { resolveBasemap, renderableBasemap, fallbackView, type Basemap, type ResolvedBasemap, } from './basemap';
4
4
  export { subscribe, disposeSubscriptions, type SubscriptionSink } from './subscribe';
package/dist/index.js CHANGED
@@ -132,55 +132,94 @@ function fallbackView(b) {
132
132
 
133
133
  // src/subscribe.ts
134
134
  import { createClient } from "graphql-ws";
135
+ var CLOSE_UNAUTHORIZED = 4401;
135
136
  function wsUrl(area) {
136
137
  const scheme = window.location.protocol === "https:" ? "wss" : "ws";
137
138
  return `${scheme}://${window.location.host}${areaPath(area)}`;
138
139
  }
139
140
  var clients = /* @__PURE__ */ new Map();
140
141
  function clientFor(area) {
141
- let client = clients.get(area);
142
- if (!client) {
143
- client = createClient({
142
+ let entry = clients.get(area);
143
+ if (!entry) {
144
+ const client = createClient({
144
145
  url: wsUrl(area),
145
146
  connectionParams: async () => {
146
147
  const token = await resolveAuthToken();
147
148
  return token ? { Authorization: `Bearer ${token}` } : {};
148
149
  }
149
150
  });
150
- clients.set(area, client);
151
+ const state = { client, acked: false, lastCloseWasAcked: false };
152
+ client.on("connecting", () => {
153
+ state.acked = false;
154
+ });
155
+ client.on("connected", () => {
156
+ state.acked = true;
157
+ });
158
+ client.on("closed", () => {
159
+ state.lastCloseWasAcked = state.acked;
160
+ state.acked = false;
161
+ });
162
+ entry = state;
163
+ clients.set(area, entry);
151
164
  }
152
- return client;
165
+ return entry;
166
+ }
167
+ function isUnauthorizedClose(err) {
168
+ return typeof err === "object" && err !== null && "code" in err && err.code === CLOSE_UNAUTHORIZED;
153
169
  }
154
170
  function subscribe(area, document, variables, sink) {
155
- const client = clientFor(area);
171
+ const entry = clientFor(area);
172
+ const { client } = entry;
173
+ let reauth = false;
156
174
  const disposers = [];
157
175
  if (sink.connected) {
158
- disposers.push(client.on("connected", (_socket, _payload, wasRetry) => sink.connected(wasRetry)));
176
+ disposers.push(
177
+ client.on("connected", (_socket, _payload, wasRetry) => {
178
+ const retried = wasRetry || reauth;
179
+ reauth = false;
180
+ sink.connected(retried);
181
+ })
182
+ );
159
183
  }
160
184
  if (sink.closed) {
161
185
  disposers.push(client.on("closed", () => sink.closed()));
162
186
  }
163
- disposers.push(
164
- client.subscribe(
165
- {
166
- query: document.toString(),
167
- variables
187
+ const payload = {
188
+ query: document.toString(),
189
+ variables
190
+ };
191
+ let disposed = false;
192
+ let current = null;
193
+ const start = () => {
194
+ const previous = current;
195
+ current = client.subscribe(payload, {
196
+ next: (result) => {
197
+ if (result.data != null) sink.next(result.data);
168
198
  },
169
- {
170
- next: (result) => {
171
- if (result.data != null) sink.next(result.data);
172
- },
173
- error: (err) => sink.error?.(err),
174
- complete: () => sink.complete?.()
175
- }
176
- )
177
- );
199
+ error: (err) => {
200
+ if (disposed) return;
201
+ if (isUnauthorizedClose(err) && entry.lastCloseWasAcked) {
202
+ reauth = true;
203
+ start();
204
+ return;
205
+ }
206
+ sink.error?.(err);
207
+ },
208
+ complete: () => sink.complete?.()
209
+ });
210
+ previous?.();
211
+ };
212
+ start();
213
+ disposers.push(() => {
214
+ disposed = true;
215
+ current?.();
216
+ });
178
217
  return () => {
179
218
  for (const dispose of disposers) dispose();
180
219
  };
181
220
  }
182
221
  function disposeSubscriptions() {
183
- for (const client of clients.values()) {
222
+ for (const { client } of clients.values()) {
184
223
  void client.dispose();
185
224
  }
186
225
  clients.clear();
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/transport.ts", "../src/errors.ts", "../src/basemap.ts", "../src/subscribe.ts", "../src/jwt.ts", "../src/tokens.ts"],
4
- "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// A tiny GraphQL-over-fetch client, in the spirit of a REST `fetchJson` helper \u2014\n// no Apollo. Each DeviceChain functional area serves its own /graphql endpoint;\n// `area` selects which one. Requests carry the access-token Bearer when one is\n// available (see setAuthTokenGetter); login/refresh run with no token (the\n// GraphQL handler lets an absent token through with no tenant).\n\nimport type { DocumentTypeDecoration } from '@graphql-typed-document-node/core';\n\n// DeviceChain functional areas that expose a GraphQL endpoint. An \"area\" ending in\n// `/admin` is a service's instance-scoped admin API (ADR-033), served by that\n// service at a second path and authenticated with the identity token rather than a\n// tenant access token (see the `identity` request option).\n//\n// `ai-inference` has no `/admin` sibling here because it has no tenant-facing\n// surface a console would call: its data plane serves exactly one mutation, reached\n// by event-processing's service token, never by a browser. Its whole console-facing\n// surface is the operator provider list (ADR-065).\n// Declared as a const array with the type derived from it, rather than as a bare\n// union, so the set is enumerable at RUNTIME. That is what lets a test walk every\n// member and check it against the deployment catalog the Helm chart uses \u2014 a\n// union alone erases at compile time, leaving the check to a hand-kept copy of\n// this list, which is the drift this whole mechanism exists to prevent.\nexport const AREAS = [\n 'user-management',\n 'user-management/admin',\n 'user-management/settings',\n 'device-management',\n 'event-management',\n 'event-processing',\n 'device-state',\n 'command-delivery',\n 'dashboard-management',\n 'outbound-connectors',\n 'ai-inference/admin',\n] as const;\n\nexport type Area = (typeof AREAS)[number];\n\n// Relative URL matching the cluster ingress contract: the ingress routes\n// https://<host>/api/<area>/graphql to each functional-area service and serves\n// the SPA at \"/\". In `vite dev` the same path is handled by the config proxy.\n// 'user-management/admin' resolves to /api/user-management/admin/graphql.\nexport function areaPath(area: Area): string {\n return `/api/${area}/graphql`;\n}\n\n/**\n * The deployed functional area an Area is served by \u2014 the segment before the\n * first slash, since a suffix like `/admin` or `/settings` names a PLANE of an\n * area rather than an area of its own.\n *\n * This is the join between an Area (a client-side routing convention) and the\n * area vocabulary the server reports from what the chart actually deployed, so\n * the console can tell \"never deployed here\" apart from a transport failure.\n * Note it is deliberately many-to-one: `user-management`,\n * `user-management/admin` and `user-management/settings` all key to\n * `user-management`, and the reverse direction does not exist \u2014 plenty of\n * deployed areas (event-sources, notification-management) have no Area member.\n */\nexport function areaKey(area: Area): string {\n return area.split('/')[0];\n}\n\n// Whether an area is served on an instance-scoped lane that authenticates with the\n// IDENTITY token rather than a tenant access token (ADR-033). Those endpoints accept\n// nothing else, so the credential follows from the area and is not a per-call\n// choice: deriving it here means a new admin call cannot silently ride the tenant\n// token and 401 (or, worse, be given a tenant token that some future endpoint\n// accepts). Callers may still pass `identity` explicitly; this is the default, not\n// an override.\nexport function isIdentityArea(area: Area): boolean {\n return area.endsWith('/admin') || area.endsWith('/settings');\n}\n\n// \u2500\u2500 Auth token injection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// A host (e.g. the console's AuthProvider) registers a getter so the client can\n// attach a fresh access token \u2014 refreshing it first if near expiry \u2014 without the\n// SDK owning React state or storage.\n\nlet tokenGetter: (() => Promise<string | null>) | null = null;\n\nexport function setAuthTokenGetter(getter: (() => Promise<string | null>) | null) {\n tokenGetter = getter;\n}\n\n// The identity-token getter feeds the admin API (ADR-033): admin requests carry\n// the instance-scoped identity token, not the tenant access token. Registered\n// alongside setAuthTokenGetter; selected per request via the `identity` option.\nlet identityTokenGetter: (() => Promise<string | null>) | null = null;\n\nexport function setIdentityTokenGetter(getter: (() => Promise<string | null>) | null) {\n identityTokenGetter = getter;\n}\n\n// resolveAuthToken returns the current access token (refreshing via the getter),\n// or null. Internal to the SDK \u2014 the ws subscribe path (subscribe.ts) reuses it\n// so subscriptions authenticate with the same token as queries.\nexport async function resolveAuthToken(): Promise<string | null> {\n return tokenGetter ? tokenGetter() : null;\n}\n\n// \u2500\u2500 Errors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class GraphQLRequestError extends Error {\n constructor(\n message: string,\n /** HTTP status, or 0 for a transport-level failure. */\n public status: number,\n /** The raw `errors` array from a GraphQL response, when present. */\n public errors?: { message: string }[],\n ) {\n super(message);\n this.name = 'GraphQLRequestError';\n }\n}\n\ninterface GraphQLResponse<T> {\n data?: T;\n errors?: { message: string }[];\n}\n\nexport interface RequestOptions {\n /** Skip attaching the Bearer token (login / refresh run unauthenticated). */\n anonymous?: boolean;\n /**\n * Authenticate with the identity token instead of the tenant access token \u2014\n * for the instance-scoped admin API (ADR-033). Ignored when `anonymous`.\n *\n * Rarely needed: an `/admin` or `/settings` area defaults to the identity token\n * already (see isIdentityArea). Pass it only to force the lane for an area whose\n * name does not say so.\n */\n identity?: boolean;\n}\n\n// A GraphQL document tagged with its result/variable types \u2014 what gql()/subscribe()\n// accept. A code-generated `TypedDocumentString` (client preset) satisfies it, and\n// so does a hand-authored query string cast to it: the SDK runs in documentMode\n// 'string', calling only `toString()` to get the query text, so the phantom result\n// (`TResult`) and variable (`TVariables`) generics are all that distinguish them.\n// Exported so packages/apps without codegen (dashboards, widgets, the dash app)\n// hand-author typed documents against one shared alias instead of re-inlining it.\nexport type TypedDocument<TResult, TVariables> = DocumentTypeDecoration<TResult, TVariables> & {\n toString(): string;\n};\n\n// `document` is a code-generated typed document (a `TypedDocumentString` from the\n// GraphQL Code Generator client preset). It implements\n// `DocumentTypeDecoration<TResult, TVariables>`, which carries the result and\n// variable types as phantom generics, and stringifies (via `toString()`) to the\n// raw query text the server expects. The structural `& { toString(): string }`\n// keeps this accepting a document from ANY service while still inferring both the\n// result (`TResult`) and the variables (`TVariables`).\nexport async function gql<TResult, TVariables>(\n area: Area,\n document: TypedDocument<TResult, TVariables>,\n ...[variables, options]: TVariables extends Record<string, never>\n ? [variables?: undefined, options?: RequestOptions]\n : [variables: TVariables, options?: RequestOptions]\n): Promise<TResult> {\n const headers: Record<string, string> = { 'Content-Type': 'application/json' };\n\n if (!options?.anonymous) {\n const useIdentity = options?.identity ?? isIdentityArea(area);\n const getter = useIdentity ? identityTokenGetter : tokenGetter;\n if (getter) {\n const token = await getter();\n if (token) headers['Authorization'] = `Bearer ${token}`;\n }\n }\n\n let res: Response;\n try {\n res = await fetch(areaPath(area), {\n method: 'POST',\n headers,\n body: JSON.stringify({ query: document.toString(), variables }),\n });\n } catch (err) {\n throw new GraphQLRequestError(\n err instanceof Error ? err.message : 'Network request failed',\n 0,\n );\n }\n\n if (!res.ok) {\n throw new GraphQLRequestError(`Request failed (${res.status})`, res.status);\n }\n\n const body = (await res.json()) as GraphQLResponse<TResult>;\n if (body.errors && body.errors.length > 0) {\n throw new GraphQLRequestError(body.errors[0].message, res.status, body.errors);\n }\n if (body.data === undefined) {\n throw new GraphQLRequestError('Empty GraphQL response', res.status);\n }\n return body.data;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Classification of transport failures a caller has to render differently.\n//\n// Most errors are one shape to a screen (\"something went wrong\"), but a refusal\n// is not: \"you may not see this\" and \"this could not be loaded\" call for\n// different copy, and collapsing them makes a permission boundary look like an\n// outage.\n//\n// This lives beside GraphQLRequestError rather than in a host app because more\n// than one consumer needs it: the console renders a device's position panel from\n// it, and the dashboard runtime's location channel turns the same refusal into a\n// first-class snapshot state (location:read is deliberately outside the read-only\n// viewer baseline, so being refused is an ordinary state rather than a fault).\n\nimport { GraphQLRequestError } from './transport';\n\n// The backend's authorization gate returns core/auth's ErrForbidden, whose text\n// is \"forbidden: missing required authority\"; graphql-go surfaces a resolver\n// error verbatim as the entry's `message`, over HTTP 200. Anchoring the match at\n// the START of the message keeps it from firing on an unrelated error that\n// merely mentions the word (an entity literally named \"forbidden-zone\", say).\nconst FORBIDDEN_PREFIX = /^forbidden\\b/i;\n\n/**\n * True when the server REFUSED the request for lack of an authority, as opposed\n * to failing it. Callers use this to render a permission state rather than an\n * error state.\n */\nexport function isForbiddenError(err: unknown): boolean {\n if (!(err instanceof GraphQLRequestError)) return false;\n // A transport-level 403 counts even with no GraphQL body to read.\n if (err.status === 403) return true;\n const messages = err.errors?.length ? err.errors.map((e) => e.message) : [err.message];\n return messages.some((m) => FORBIDDEN_PREFIX.test(m.trim()));\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The client half of the basemap cascade (ADR-079).\n//\n// The SERVER resolves a tenant's override over the operator's instance default and\n// hands back one effective value. This module resolves the last tier \u2014 a per-surface\n// override, set on the widget or held as a personal preference in the browser \u2014 over\n// that tenant value, and turns the result into something a renderer can use.\n//\n// It lives in the SDK rather than in either app because THREE surfaces need the\n// identical answer: the console's geofence editor, the dashboard map widget, and the\n// `/dash` viewer app, which has its own login and would otherwise be the one place\n// that quietly disagrees.\n\n/** A basemap at any tier: the tile source, and a fallback view. */\nexport interface Basemap {\n tileUrl?: string | null;\n attribution?: string | null;\n centerLat?: number | null;\n centerLon?: number | null;\n zoom?: number | null;\n}\n\n/** A resolved basemap that definitely has a tile source, so a renderer can draw it. */\nexport interface ResolvedBasemap {\n tileUrl: string;\n attribution: string | null;\n centerLat: number | null;\n centerLon: number | null;\n zoom: number | null;\n}\n\n/** Treat blank and whitespace-only strings as unset \u2014 an emptied form field means \"inherit\". */\nfunction text(v: string | null | undefined): string | null {\n if (typeof v !== 'string') return null;\n const trimmed = v.trim();\n return trimmed === '' ? null : trimmed;\n}\n\nfunction num(v: number | null | undefined): number | null {\n return typeof v === 'number' && Number.isFinite(v) ? v : null;\n}\n\n/**\n * Fold a per-surface override over the tenant's effective basemap.\n *\n * \uD83D\uDD34 The TILE SOURCE is atomic, exactly as it is on the server. If the override names\n * a tile URL, its attribution comes from the override too \u2014 never from the tenant. A\n * per-field fold would credit the tenant's provider for the override's tiles, which\n * is a licence violation manufactured by the merge rather than a cosmetic mismatch.\n * This is the same rule as basemap.Merge in user-management, and it has to be\n * restated here because this tier never reaches the server.\n *\n * The camera folds per field: a widget that only wants to open closer in should not\n * have to restate a provider to keep it.\n */\nexport function resolveBasemap(override: Basemap | null | undefined, tenant: Basemap | null | undefined): Basemap {\n const o = override ?? {};\n const t = tenant ?? {};\n\n // \uD83D\uDD34 AN INCOMPLETE OVERRIDE IS NOT AN OVERRIDE. A tile URL with no credit line is\n // refused outright by the server at the tenant and instance tiers \u2014 the two halves\n // are stored as one value or not at all \u2014 but THIS tier never reaches the server: a\n // widget's own options and the fence editor's browser-local fields are entered\n // client-side and rendered directly.\n //\n // Without this the atomic fold below did its job and still produced the defect it\n // exists to prevent: it correctly refused to lend the tenant's credit line to the\n // override's tiles, and then drew those tiles with NO credit line at all. Correct\n // about the cross-crediting, wrong about the licence.\n //\n // So an override naming tiles without their attribution is discarded whole, and the\n // surface falls back to the tenant's properly-credited basemap. Failing closed to a\n // map that is credited beats honouring a preference that cannot legally be shown.\n //\n // On an ordinary instance the fallback is a real map, because a shipped default\n // means some tier below almost always has one. On an instance whose operator\n // deliberately stored `{}` there is nothing below, and the surface falls through to\n // the bundled schematic world \u2014 which is the correct outcome for the same reason,\n // just a less pretty one.\n const overrideAttribution = text(o.attribution);\n const overrideTiles = overrideAttribution === null ? null : text(o.tileUrl);\n const tileUrl = overrideTiles ?? text(t.tileUrl);\n const attribution = overrideTiles ? overrideAttribution : text(t.attribution);\n\n // Latitude and longitude move as a pair for the same reason: half a coordinate\n // from each tier names a point neither one chose.\n const hasOverrideCentre = num(o.centerLat) !== null || num(o.centerLon) !== null;\n const centerLat = hasOverrideCentre ? num(o.centerLat) : num(t.centerLat);\n const centerLon = hasOverrideCentre ? num(o.centerLon) : num(t.centerLon);\n\n return {\n tileUrl,\n attribution,\n centerLat,\n centerLon,\n zoom: num(o.zoom) ?? num(t.zoom),\n };\n}\n\n/**\n * Narrow a folded basemap to one that can actually be rendered, or null.\n *\n * A basemap with a camera but no tile source is legitimate \u2014 an operator may set a\n * starting view before choosing a provider \u2014 but there is nothing to draw, and the\n * caller should say so rather than request tiles from an empty URL.\n */\nexport function renderableBasemap(b: Basemap | null | undefined): ResolvedBasemap | null {\n const tileUrl = text(b?.tileUrl);\n if (!tileUrl) return null;\n return {\n tileUrl,\n attribution: text(b?.attribution),\n centerLat: num(b?.centerLat),\n centerLon: num(b?.centerLon),\n zoom: num(b?.zoom),\n };\n}\n\n/**\n * The fallback view a surface opens at when it has nothing of its own to fit to.\n *\n * \uD83D\uDD34 A FALLBACK, never an override (ADR-079 \u00A75). A surface with real content \u2014 an\n * existing fence's ring, a widget's markers \u2014 fits that and ignores this entirely.\n * Returns null when no tier declared a centre, so the caller keeps its own default\n * rather than being moved to 0,0.\n */\nexport function fallbackView(b: Basemap | null | undefined): { center: [number, number]; zoom: number | null } | null {\n const lat = num(b?.centerLat);\n const lon = num(b?.centerLon);\n if (lat === null || lon === null) return null;\n return { center: [lon, lat], zoom: num(b?.zoom) };\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// GraphQL subscriptions over the graphql-transport-ws protocol (ADR-037) \u2014 the\n// live half of the client wire, consumed by dashboards (the ADR-039 Hub) for\n// live telemetry. The backend serves subscriptions on the same /api/<area>/graphql\n// path via a WebSocket upgrade; a client derives the ws:// URL from the http one.\n//\n// This is the low-level SDK primitive: one lazily-created graphql-ws client per\n// area (connection reuse), and a subscribe() that binds a typed document to a\n// sink. The dashboard Hub layers subscription multiplexing on top (many widgets,\n// one connection).\n\nimport type { DocumentTypeDecoration } from '@graphql-typed-document-node/core';\nimport { createClient, type Client } from 'graphql-ws';\n\nimport { areaPath, resolveAuthToken, type Area } from './transport';\n\n// wsUrl builds the absolute ws(s):// URL for an area from the current origin,\n// mirroring the relative http path so dev proxy and prod ingress both work.\nfunction wsUrl(area: Area): string {\n const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws';\n return `${scheme}://${window.location.host}${areaPath(area)}`;\n}\n\n// One graphql-ws client per area \u2014 the WebSocket (and its connection_init auth)\n// is reused across every subscription to that area. connectionParams is a\n// function so the token is re-resolved on each (re)connect, surviving refresh.\nconst clients = new Map<Area, Client>();\n\nfunction clientFor(area: Area): Client {\n let client = clients.get(area);\n if (!client) {\n client = createClient({\n url: wsUrl(area),\n connectionParams: async () => {\n const token = await resolveAuthToken();\n return token ? { Authorization: `Bearer ${token}` } : {};\n },\n });\n clients.set(area, client);\n }\n return client;\n}\n\nexport interface SubscriptionSink<T> {\n next: (data: T) => void;\n error?: (err: unknown) => void;\n complete?: () => void;\n // Connection-level signals for the shared per-area socket, distinct from the\n // per-operation next/error/complete above. `connected` fires on each successful\n // connection_ack \u2014 `wasRetry` distinguishes a reconnect (after a dropped socket)\n // from the first connect; `closed` fires on each socket close (graphql-ws then\n // auto-retries a transient close). A consumer that only needs data can omit both;\n // a live-status indicator uses them to tell \"connected but idle\" from \"offline\".\n connected?: (wasRetry: boolean) => void;\n closed?: () => void;\n}\n\n// subscribe binds a typed subscription document to a sink and returns an\n// unsubscribe function. The document is the same code-generated typed document\n// the query path uses, so the result type is inferred. Errors from the operation\n// (or the socket) surface via sink.error; normal termination via sink.complete.\nexport function subscribe<TResult, TVariables>(\n area: Area,\n document: DocumentTypeDecoration<TResult, TVariables> & { toString(): string },\n variables: TVariables extends Record<string, never> ? undefined : TVariables,\n sink: SubscriptionSink<TResult>,\n): () => void {\n const client = clientFor(area);\n // Client-level connection events are registered per subscribe() and torn down with\n // it, alongside the operation itself, so a single dispose() call releases both.\n const disposers: Array<() => void> = [];\n if (sink.connected) {\n disposers.push(client.on('connected', (_socket, _payload, wasRetry) => sink.connected!(wasRetry)));\n }\n if (sink.closed) {\n disposers.push(client.on('closed', () => sink.closed!()));\n }\n disposers.push(\n client.subscribe<TResult>(\n {\n query: document.toString(),\n variables: variables as Record<string, unknown> | undefined,\n },\n {\n next: (result) => {\n if (result.data != null) sink.next(result.data);\n },\n error: (err) => sink.error?.(err),\n complete: () => sink.complete?.(),\n },\n ),\n );\n return () => {\n for (const dispose of disposers) dispose();\n };\n}\n\n// disposeSubscriptions tears down every cached area client (their sockets). A\n// host calls this on sign-out so a stale token's socket does not linger.\nexport function disposeSubscriptions(): void {\n for (const client of clients.values()) {\n void client.dispose();\n }\n clients.clear();\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Client-side decode of the DeviceChain access token (ADR-008 JWT). This reads\n// the payload for display + routing only; it does NOT verify the signature \u2014\n// every protected operation is still enforced server-side by the token's\n// authorities. Never trust these claims for authorization decisions beyond what\n// the UI shows.\n\nexport interface DecodedClaims {\n /** Tenant the subject acts within (authoritative server-side). */\n tenant: string;\n /** Human-readable subject identifier. */\n username: string;\n /** Assigned role names \u2014 display/audit only; enforcement is on authorities. */\n roles: string[];\n /** Effective capabilities (e.g. \"device:write\", or \"*\" for super-authority). */\n authorities: string[];\n /** \"access\" | \"refresh\". */\n typ: string;\n /** Expiry, epoch seconds (from the standard `exp` claim). */\n exp?: number;\n}\n\nfunction base64UrlDecode(segment: string): string {\n const padded = segment.replace(/-/g, '+').replace(/_/g, '/');\n const json = atob(padded);\n // Handle UTF-8 payloads.\n return decodeURIComponent(\n Array.prototype.map\n .call(json, (c: string) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))\n .join(''),\n );\n}\n\n/** Decode a JWT's payload, or null if it is malformed. */\nexport function decodeToken(token: string): DecodedClaims | null {\n const parts = token.split('.');\n if (parts.length !== 3) return null;\n try {\n const payload = JSON.parse(base64UrlDecode(parts[1]));\n return {\n tenant: payload.tenant ?? '',\n username: payload.username ?? '',\n roles: payload.roles ?? [],\n authorities: payload.authorities ?? [],\n typ: payload.typ ?? '',\n exp: payload.exp,\n };\n } catch {\n return null;\n }\n}\n\n/** True if the token is missing, malformed, or within `skewSeconds` of expiry. */\nexport function isExpired(token: string, skewSeconds = 30): boolean {\n const claims = decodeToken(token);\n if (!claims || claims.exp === undefined) return true;\n const nowSeconds = Date.now() / 1000;\n return claims.exp - skewSeconds <= nowSeconds;\n}\n\n/** True if the claims grant the authority (the super-authority \"*\" passes all). */\nexport function hasAuthority(claims: DecodedClaims | null, required: string): boolean {\n if (!claims) return false;\n return claims.authorities.some((a) => a === '*' || a === required);\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Token masks (ADR-042 P3): the client-side template language that mints, and\n// soft-validates, entity tokens. A mask is literal text plus typed placeholders,\n// and one mask drives three operations:\n//\n// generate fill placeholders to produce a concrete token\n// normalize kebab-case a human string into a {slug}\n// validate does a token conform to the mask's shape (soft, UI-only)\n//\n// Masks are advisory: the backend enforces only the security grammar\n// (isValidToken below mirrors rdb.ValidateToken). Style \u2014 lowercase-kebab,\n// prefixes \u2014 lives in the mask, applied here in the console, never on the wire.\n\n// The human-readable alphabet for generated ids: lowercase letters + digits minus\n// the ambiguous 0 o 1 l i, so a minted token can be read back off a label without\n// transcription errors.\nconst READABLE_ALPHABET = 'abcdefghjkmnpqrstuvwxyz23456789';\nconst DIGITS = '0123456789';\n\n// The security grammar the backend enforces (rdb.ValidateToken): letters (either\n// case), digits, hyphen and underscore, starting alphanumeric. Kept in sync by\n// hand \u2014 the same regex, mirrored for a pre-submit check so the user sees a bad\n// token before the server rejects it.\nconst TOKEN_GRAMMAR = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;\n\n/** The maximum token length (mirrors rdb.MaxTokenLen). */\nexport const MAX_TOKEN_LEN = 128;\n\n/** isValidToken mirrors the backend security grammar for a pre-submit check. */\nexport function isValidToken(token: string): boolean {\n return token.length > 0 && token.length <= MAX_TOKEN_LEN && TOKEN_GRAMMAR.test(token);\n}\n\n/** A parsed mask segment: a literal run, or a typed placeholder. */\nexport type MaskSegment =\n | { kind: 'literal'; text: string }\n | { kind: 'placeholder'; type: 'alphanumeric' | 'numeric' | 'slug' | 'uuid' | 'unknown'; n?: number; raw: string };\n\nconst PLACEHOLDER = /\\{([a-zA-Z]+)(?:-(\\d+))?\\}/g;\nconst KNOWN = ['alphanumeric', 'numeric', 'slug', 'uuid'] as const;\n\n/** parseMask splits a mask into its literal and placeholder segments. */\nexport function parseMask(mask: string): MaskSegment[] {\n const segments: MaskSegment[] = [];\n let last = 0;\n for (const m of mask.matchAll(PLACEHOLDER)) {\n const at = m.index ?? 0;\n if (at > last) segments.push({ kind: 'literal', text: mask.slice(last, at) });\n const name = m[1].toLowerCase();\n const type = (KNOWN as readonly string[]).includes(name) ? (name as 'alphanumeric') : 'unknown';\n segments.push({ kind: 'placeholder', type, n: m[2] ? parseInt(m[2], 10) : undefined, raw: m[0] });\n last = at + m[0].length;\n }\n if (last < mask.length) segments.push({ kind: 'literal', text: mask.slice(last) });\n return segments;\n}\n\nexport interface GenerateOptions {\n /** A human string (e.g. the entity's name) that fills {slug} placeholders. */\n seed?: string;\n /** Injectable RNG in [0,1) for deterministic tests; defaults to Math.random. */\n random?: () => number;\n /** Injectable uuid for deterministic tests; defaults to crypto.randomUUID. */\n uuid?: () => string;\n}\n\nfunction randomUUID(): string {\n const g = globalThis as { crypto?: { randomUUID?: () => string } };\n if (g.crypto?.randomUUID) return g.crypto.randomUUID();\n // Fallback for environments without Web Crypto (not the console; belt & braces).\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\n/**\n * generateToken fills a mask's placeholders to produce a concrete token:\n * {alphanumeric-N} \u2192 N readable chars, {numeric-N} \u2192 N digits, {uuid} \u2192 a uuid,\n * {slug} \u2192 the normalized seed (or a short readable id when no seed is given, so\n * a \"regenerate\" button always yields something). Literals pass through.\n */\nexport function generateToken(mask: string, opts: GenerateOptions = {}): string {\n const rand = opts.random ?? Math.random;\n const uuid = opts.uuid ?? randomUUID;\n // \uD83D\uDD34 The width is bounded before it is used. A mask may name any width the\n // regexp matches, and `Array.from({length: 1e20})` THROWS RangeError \u2014 which\n // made this function, called during render by the settings editor and by every\n // create form, a way to kill the page from a stored value. No width past\n // MAX_TOKEN_LEN can mint a legal token anyway, so an absurd one contributes\n // nothing rather than exploding. validateMask refuses such a mask outright;\n // this keeps generation safe for a mask stored before it did.\n const pick = (alphabet: string, n: number) =>\n n > MAX_TOKEN_LEN\n ? ''\n : Array.from({ length: n }, () => alphabet[Math.floor(rand() * alphabet.length)]).join('');\n\n return parseMask(mask)\n .map((seg) => {\n if (seg.kind === 'literal') return seg.text;\n switch (seg.type) {\n case 'alphanumeric':\n return pick(READABLE_ALPHABET, seg.n ?? 8);\n case 'numeric':\n return pick(DIGITS, seg.n ?? 4);\n case 'slug':\n return opts.seed ? normalizeToken(opts.seed) : pick(READABLE_ALPHABET, 6);\n case 'uuid':\n return uuid();\n default:\n return ''; // an unknown placeholder contributes nothing to a generated token\n }\n })\n .join('');\n}\n\n/**\n * normalizeToken kebab-cases a human string into a slug: lower-case, spaces and\n * underscores to hyphens, drop anything else, collapse and trim hyphens.\n * \"Ops Overview\" \u2192 \"ops-overview\".\n */\nexport function normalizeToken(input: string): string {\n return input\n .toLowerCase()\n .trim()\n .replace(/[\\s_]+/g, '-')\n .replace(/[^a-z0-9-]+/g, '')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * maskToRegExp compiles a mask into a whole-string RegExp for soft, UI-only\n * conformance checking. It is intentionally lenient (accepts either case in\n * alphanumeric/uuid segments) \u2014 the backend, not this, is the authority.\n */\nexport function maskToRegExp(mask: string): RegExp {\n const body = parseMask(mask)\n .map((seg) => {\n if (seg.kind === 'literal') return escapeRegExp(seg.text);\n switch (seg.type) {\n case 'alphanumeric':\n return `[A-Za-z0-9]{${seg.n ?? 8}}`;\n case 'numeric':\n return `[0-9]{${seg.n ?? 4}}`;\n case 'slug':\n return `[a-z0-9]+(?:-[a-z0-9]+)*`;\n case 'uuid':\n return `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`;\n default:\n return escapeRegExp(seg.raw); // unknown placeholder must match itself literally\n }\n })\n .join('');\n return new RegExp(`^${body}$`);\n}\n\n/** conformsToMask reports whether a token matches the mask's shape (soft check). */\nexport function conformsToMask(mask: string, token: string): boolean {\n return maskToRegExp(mask).test(token);\n}\n\n/** The fixed seed sampleToken fills {slug} placeholders from. */\nconst SAMPLE_SEED = 'Sample Name';\n\n/**\n * sampleToken generates the token a mask would mint, deterministically. Same\n * generator as generateToken \u2014 a pinned RNG and uuid rather than a second\n * implementation, so a sample can never disagree with what a create form\n * actually produces.\n */\nexport function sampleToken(mask: string, seed: string = SAMPLE_SEED): string {\n return generateToken(mask, {\n seed,\n random: () => 0,\n uuid: () => '3f2b8c14-9d7e-4a51-b6c2-0e8d5a1f7b93',\n });\n}\n\n/**\n * Why a mask cannot mint usable tokens. Structured rather than a message\n * because this package is consumed by localized apps \u2014 the caller maps a reason\n * to its own wording.\n */\nexport type MaskProblem =\n | { reason: 'empty' }\n | { reason: 'unknownPlaceholder'; placeholder: string }\n | { reason: 'noPlaceholder' }\n | { reason: 'widthTooLarge'; placeholder: string; max: number }\n | { reason: 'invalidToken'; sample: string };\n\n/**\n * validateMask reports why a mask is unusable, or null when it is fine. It\n * mirrors the server (the tokenmask Go package), which is the authority and\n * refuses the same four things \u2014 every one of which fails SILENTLY otherwise:\n *\n * empty nothing to mint from\n * unknownPlaceholder contributes nothing, so \"dev-{sulg}\" mints \"dev-\"\n * noPlaceholder every entity mints the SAME token; the second create collides\n * invalidToken the minted sample is not a legal token (a space, a slash\u2026)\n *\n * \uD83D\uDD34 Keep this NO STRICTER than the server. A check the server does not make\n * would refuse a mask the platform accepts, and the operator would have no way\n * to get past it.\n */\nexport function validateMask(mask: string): MaskProblem | null {\n if (mask === '') return { reason: 'empty' };\n let placeholders = 0;\n for (const seg of parseMask(mask)) {\n if (seg.kind !== 'placeholder') continue;\n if (seg.type === 'unknown') return { reason: 'unknownPlaceholder', placeholder: seg.raw };\n // \uD83D\uDD34 Checked BEFORE anything samples the mask. This function is called during\n // render, per keystroke \u2014 a width of 5e7 froze the tab for seconds building a\n // string nobody would ever see, and 1e20 threw RangeError and killed the\n // page. Neither reached the `invalidToken` check below, which is where a\n // reader would expect an over-long token to be caught.\n if (seg.n !== undefined && seg.n > MAX_TOKEN_LEN) {\n return { reason: 'widthTooLarge', placeholder: seg.raw, max: MAX_TOKEN_LEN };\n }\n placeholders++;\n }\n if (placeholders === 0) return { reason: 'noPlaceholder' };\n const sample = sampleToken(mask);\n if (!isValidToken(sample)) return { reason: 'invalidToken', sample };\n return null;\n}\n\n/**\n * resolveMask picks the mask for an entity type from a token-masks map, falling\n * back to the \"default\" entry and finally to a bare {slug}.\n */\nexport function resolveMask(masks: Record<string, string>, entityType: string): string {\n return masks[entityType] ?? masks.default ?? '{slug}';\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// A tiny GraphQL-over-fetch client, in the spirit of a REST `fetchJson` helper \u2014\n// no Apollo. Each DeviceChain functional area serves its own /graphql endpoint;\n// `area` selects which one. Requests carry the access-token Bearer when one is\n// available (see setAuthTokenGetter); login/refresh run with no token (the\n// GraphQL handler lets an absent token through with no tenant).\n\nimport type { DocumentTypeDecoration } from '@graphql-typed-document-node/core';\n\n// DeviceChain functional areas that expose a GraphQL endpoint. An \"area\" ending in\n// `/admin` is a service's instance-scoped admin API (ADR-033), served by that\n// service at a second path and authenticated with the identity token rather than a\n// tenant access token (see the `identity` request option).\n//\n// `ai-inference` has no `/admin` sibling here because it has no tenant-facing\n// surface a console would call: its data plane serves exactly one mutation, reached\n// by event-processing's service token, never by a browser. Its whole console-facing\n// surface is the operator provider list (ADR-065).\n// Declared as a const array with the type derived from it, rather than as a bare\n// union, so the set is enumerable at RUNTIME. That is what lets a test walk every\n// member and check it against the deployment catalog the Helm chart uses \u2014 a\n// union alone erases at compile time, leaving the check to a hand-kept copy of\n// this list, which is the drift this whole mechanism exists to prevent.\nexport const AREAS = [\n 'user-management',\n 'user-management/admin',\n 'user-management/settings',\n 'device-management',\n 'event-management',\n 'event-processing',\n 'device-state',\n 'command-delivery',\n 'dashboard-management',\n 'outbound-connectors',\n 'ai-inference/admin',\n] as const;\n\nexport type Area = (typeof AREAS)[number];\n\n// Relative URL matching the cluster ingress contract: the ingress routes\n// https://<host>/api/<area>/graphql to each functional-area service and serves\n// the SPA at \"/\". In `vite dev` the same path is handled by the config proxy.\n// 'user-management/admin' resolves to /api/user-management/admin/graphql.\nexport function areaPath(area: Area): string {\n return `/api/${area}/graphql`;\n}\n\n/**\n * The deployed functional area an Area is served by \u2014 the segment before the\n * first slash, since a suffix like `/admin` or `/settings` names a PLANE of an\n * area rather than an area of its own.\n *\n * This is the join between an Area (a client-side routing convention) and the\n * area vocabulary the server reports from what the chart actually deployed, so\n * the console can tell \"never deployed here\" apart from a transport failure.\n * Note it is deliberately many-to-one: `user-management`,\n * `user-management/admin` and `user-management/settings` all key to\n * `user-management`, and the reverse direction does not exist \u2014 plenty of\n * deployed areas (event-sources, notification-management) have no Area member.\n */\nexport function areaKey(area: Area): string {\n return area.split('/')[0];\n}\n\n// Whether an area is served on an instance-scoped lane that authenticates with the\n// IDENTITY token rather than a tenant access token (ADR-033). Those endpoints accept\n// nothing else, so the credential follows from the area and is not a per-call\n// choice: deriving it here means a new admin call cannot silently ride the tenant\n// token and 401 (or, worse, be given a tenant token that some future endpoint\n// accepts). Callers may still pass `identity` explicitly; this is the default, not\n// an override.\nexport function isIdentityArea(area: Area): boolean {\n return area.endsWith('/admin') || area.endsWith('/settings');\n}\n\n// \u2500\u2500 Auth token injection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// A host (e.g. the console's AuthProvider) registers a getter so the client can\n// attach a fresh access token \u2014 refreshing it first if near expiry \u2014 without the\n// SDK owning React state or storage.\n\nlet tokenGetter: (() => Promise<string | null>) | null = null;\n\nexport function setAuthTokenGetter(getter: (() => Promise<string | null>) | null) {\n tokenGetter = getter;\n}\n\n// The identity-token getter feeds the admin API (ADR-033): admin requests carry\n// the instance-scoped identity token, not the tenant access token. Registered\n// alongside setAuthTokenGetter; selected per request via the `identity` option.\nlet identityTokenGetter: (() => Promise<string | null>) | null = null;\n\nexport function setIdentityTokenGetter(getter: (() => Promise<string | null>) | null) {\n identityTokenGetter = getter;\n}\n\n// resolveAuthToken returns the current access token (refreshing via the getter),\n// or null. Internal to the SDK \u2014 the ws subscribe path (subscribe.ts) reuses it\n// so subscriptions authenticate with the same token as queries.\nexport async function resolveAuthToken(): Promise<string | null> {\n return tokenGetter ? tokenGetter() : null;\n}\n\n// \u2500\u2500 Errors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class GraphQLRequestError extends Error {\n constructor(\n message: string,\n /** HTTP status, or 0 for a transport-level failure. */\n public status: number,\n /** The raw `errors` array from a GraphQL response, when present. */\n public errors?: GraphQLErrorEntry[],\n ) {\n super(message);\n this.name = 'GraphQLRequestError';\n }\n}\n\n/**\n * One entry of a GraphQL response's `errors` array. `extensions` carries the server's\n * machine-readable detail when it sends any \u2014 for example `code: \"THROTTLED\"` with\n * `retryAfterSeconds` on a sign-in refused by the per-account backoff \u2014 so a caller can\n * branch on a code instead of matching the message text.\n */\nexport interface GraphQLErrorEntry {\n message: string;\n extensions?: Record<string, unknown>;\n}\n\ninterface GraphQLResponse<T> {\n data?: T;\n errors?: GraphQLErrorEntry[];\n}\n\nexport interface RequestOptions {\n /** Skip attaching the Bearer token (login / refresh run unauthenticated). */\n anonymous?: boolean;\n /**\n * Authenticate with the identity token instead of the tenant access token \u2014\n * for the instance-scoped admin API (ADR-033). Ignored when `anonymous`.\n *\n * Rarely needed: an `/admin` or `/settings` area defaults to the identity token\n * already (see isIdentityArea). Pass it only to force the lane for an area whose\n * name does not say so.\n */\n identity?: boolean;\n}\n\n// A GraphQL document tagged with its result/variable types \u2014 what gql()/subscribe()\n// accept. A code-generated `TypedDocumentString` (client preset) satisfies it, and\n// so does a hand-authored query string cast to it: the SDK runs in documentMode\n// 'string', calling only `toString()` to get the query text, so the phantom result\n// (`TResult`) and variable (`TVariables`) generics are all that distinguish them.\n// Exported so packages/apps without codegen (dashboards, widgets, the dash app)\n// hand-author typed documents against one shared alias instead of re-inlining it.\nexport type TypedDocument<TResult, TVariables> = DocumentTypeDecoration<TResult, TVariables> & {\n toString(): string;\n};\n\n// `document` is a code-generated typed document (a `TypedDocumentString` from the\n// GraphQL Code Generator client preset). It implements\n// `DocumentTypeDecoration<TResult, TVariables>`, which carries the result and\n// variable types as phantom generics, and stringifies (via `toString()`) to the\n// raw query text the server expects. The structural `& { toString(): string }`\n// keeps this accepting a document from ANY service while still inferring both the\n// result (`TResult`) and the variables (`TVariables`).\nexport async function gql<TResult, TVariables>(\n area: Area,\n document: TypedDocument<TResult, TVariables>,\n ...[variables, options]: TVariables extends Record<string, never>\n ? [variables?: undefined, options?: RequestOptions]\n : [variables: TVariables, options?: RequestOptions]\n): Promise<TResult> {\n const headers: Record<string, string> = { 'Content-Type': 'application/json' };\n\n if (!options?.anonymous) {\n const useIdentity = options?.identity ?? isIdentityArea(area);\n const getter = useIdentity ? identityTokenGetter : tokenGetter;\n if (getter) {\n const token = await getter();\n if (token) headers['Authorization'] = `Bearer ${token}`;\n }\n }\n\n let res: Response;\n try {\n res = await fetch(areaPath(area), {\n method: 'POST',\n headers,\n body: JSON.stringify({ query: document.toString(), variables }),\n });\n } catch (err) {\n throw new GraphQLRequestError(\n err instanceof Error ? err.message : 'Network request failed',\n 0,\n );\n }\n\n if (!res.ok) {\n throw new GraphQLRequestError(`Request failed (${res.status})`, res.status);\n }\n\n const body = (await res.json()) as GraphQLResponse<TResult>;\n if (body.errors && body.errors.length > 0) {\n throw new GraphQLRequestError(body.errors[0].message, res.status, body.errors);\n }\n if (body.data === undefined) {\n throw new GraphQLRequestError('Empty GraphQL response', res.status);\n }\n return body.data;\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Classification of transport failures a caller has to render differently.\n//\n// Most errors are one shape to a screen (\"something went wrong\"), but a refusal\n// is not: \"you may not see this\" and \"this could not be loaded\" call for\n// different copy, and collapsing them makes a permission boundary look like an\n// outage.\n//\n// This lives beside GraphQLRequestError rather than in a host app because more\n// than one consumer needs it: the console renders a device's position panel from\n// it, and the dashboard runtime's location channel turns the same refusal into a\n// first-class snapshot state (location:read is deliberately outside the read-only\n// viewer baseline, so being refused is an ordinary state rather than a fault).\n\nimport { GraphQLRequestError } from './transport';\n\n// The backend's authorization gate returns core/auth's ErrForbidden, whose text\n// is \"forbidden: missing required authority\"; graphql-go surfaces a resolver\n// error verbatim as the entry's `message`, over HTTP 200. Anchoring the match at\n// the START of the message keeps it from firing on an unrelated error that\n// merely mentions the word (an entity literally named \"forbidden-zone\", say).\nconst FORBIDDEN_PREFIX = /^forbidden\\b/i;\n\n/**\n * True when the server REFUSED the request for lack of an authority, as opposed\n * to failing it. Callers use this to render a permission state rather than an\n * error state.\n */\nexport function isForbiddenError(err: unknown): boolean {\n if (!(err instanceof GraphQLRequestError)) return false;\n // A transport-level 403 counts even with no GraphQL body to read.\n if (err.status === 403) return true;\n const messages = err.errors?.length ? err.errors.map((e) => e.message) : [err.message];\n return messages.some((m) => FORBIDDEN_PREFIX.test(m.trim()));\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// The client half of the basemap cascade (ADR-079).\n//\n// The SERVER resolves a tenant's override over the operator's instance default and\n// hands back one effective value. This module resolves the last tier \u2014 a per-surface\n// override, set on the widget or held as a personal preference in the browser \u2014 over\n// that tenant value, and turns the result into something a renderer can use.\n//\n// It lives in the SDK rather than in either app because THREE surfaces need the\n// identical answer: the console's geofence editor, the dashboard map widget, and the\n// `/dash` viewer app, which has its own login and would otherwise be the one place\n// that quietly disagrees.\n\n/** A basemap at any tier: the tile source, and a fallback view. */\nexport interface Basemap {\n tileUrl?: string | null;\n attribution?: string | null;\n centerLat?: number | null;\n centerLon?: number | null;\n zoom?: number | null;\n}\n\n/** A resolved basemap that definitely has a tile source, so a renderer can draw it. */\nexport interface ResolvedBasemap {\n tileUrl: string;\n attribution: string | null;\n centerLat: number | null;\n centerLon: number | null;\n zoom: number | null;\n}\n\n/** Treat blank and whitespace-only strings as unset \u2014 an emptied form field means \"inherit\". */\nfunction text(v: string | null | undefined): string | null {\n if (typeof v !== 'string') return null;\n const trimmed = v.trim();\n return trimmed === '' ? null : trimmed;\n}\n\nfunction num(v: number | null | undefined): number | null {\n return typeof v === 'number' && Number.isFinite(v) ? v : null;\n}\n\n/**\n * Fold a per-surface override over the tenant's effective basemap.\n *\n * \uD83D\uDD34 The TILE SOURCE is atomic, exactly as it is on the server. If the override names\n * a tile URL, its attribution comes from the override too \u2014 never from the tenant. A\n * per-field fold would credit the tenant's provider for the override's tiles, which\n * is a licence violation manufactured by the merge rather than a cosmetic mismatch.\n * This is the same rule as basemap.Merge in user-management, and it has to be\n * restated here because this tier never reaches the server.\n *\n * The camera folds per field: a widget that only wants to open closer in should not\n * have to restate a provider to keep it.\n */\nexport function resolveBasemap(override: Basemap | null | undefined, tenant: Basemap | null | undefined): Basemap {\n const o = override ?? {};\n const t = tenant ?? {};\n\n // \uD83D\uDD34 AN INCOMPLETE OVERRIDE IS NOT AN OVERRIDE. A tile URL with no credit line is\n // refused outright by the server at the tenant and instance tiers \u2014 the two halves\n // are stored as one value or not at all \u2014 but THIS tier never reaches the server: a\n // widget's own options and the fence editor's browser-local fields are entered\n // client-side and rendered directly.\n //\n // Without this the atomic fold below did its job and still produced the defect it\n // exists to prevent: it correctly refused to lend the tenant's credit line to the\n // override's tiles, and then drew those tiles with NO credit line at all. Correct\n // about the cross-crediting, wrong about the licence.\n //\n // So an override naming tiles without their attribution is discarded whole, and the\n // surface falls back to the tenant's properly-credited basemap. Failing closed to a\n // map that is credited beats honouring a preference that cannot legally be shown.\n //\n // On an ordinary instance the fallback is a real map, because a shipped default\n // means some tier below almost always has one. On an instance whose operator\n // deliberately stored `{}` there is nothing below, and the surface falls through to\n // the bundled schematic world \u2014 which is the correct outcome for the same reason,\n // just a less pretty one.\n const overrideAttribution = text(o.attribution);\n const overrideTiles = overrideAttribution === null ? null : text(o.tileUrl);\n const tileUrl = overrideTiles ?? text(t.tileUrl);\n const attribution = overrideTiles ? overrideAttribution : text(t.attribution);\n\n // Latitude and longitude move as a pair for the same reason: half a coordinate\n // from each tier names a point neither one chose.\n const hasOverrideCentre = num(o.centerLat) !== null || num(o.centerLon) !== null;\n const centerLat = hasOverrideCentre ? num(o.centerLat) : num(t.centerLat);\n const centerLon = hasOverrideCentre ? num(o.centerLon) : num(t.centerLon);\n\n return {\n tileUrl,\n attribution,\n centerLat,\n centerLon,\n zoom: num(o.zoom) ?? num(t.zoom),\n };\n}\n\n/**\n * Narrow a folded basemap to one that can actually be rendered, or null.\n *\n * A basemap with a camera but no tile source is legitimate \u2014 an operator may set a\n * starting view before choosing a provider \u2014 but there is nothing to draw, and the\n * caller should say so rather than request tiles from an empty URL.\n */\nexport function renderableBasemap(b: Basemap | null | undefined): ResolvedBasemap | null {\n const tileUrl = text(b?.tileUrl);\n if (!tileUrl) return null;\n return {\n tileUrl,\n attribution: text(b?.attribution),\n centerLat: num(b?.centerLat),\n centerLon: num(b?.centerLon),\n zoom: num(b?.zoom),\n };\n}\n\n/**\n * The fallback view a surface opens at when it has nothing of its own to fit to.\n *\n * \uD83D\uDD34 A FALLBACK, never an override (ADR-079 \u00A75). A surface with real content \u2014 an\n * existing fence's ring, a widget's markers \u2014 fits that and ignores this entirely.\n * Returns null when no tier declared a centre, so the caller keeps its own default\n * rather than being moved to 0,0.\n */\nexport function fallbackView(b: Basemap | null | undefined): { center: [number, number]; zoom: number | null } | null {\n const lat = num(b?.centerLat);\n const lon = num(b?.centerLon);\n if (lat === null || lon === null) return null;\n return { center: [lon, lat], zoom: num(b?.zoom) };\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// GraphQL subscriptions over the graphql-transport-ws protocol (ADR-037) \u2014 the\n// live half of the client wire, consumed by dashboards (the ADR-039 Hub) for\n// live telemetry. The backend serves subscriptions on the same /api/<area>/graphql\n// path via a WebSocket upgrade; a client derives the ws:// URL from the http one.\n//\n// This is the low-level SDK primitive: one lazily-created graphql-ws client per\n// area (connection reuse), and a subscribe() that binds a typed document to a\n// sink. The dashboard Hub layers subscription multiplexing on top (many widgets,\n// one connection).\n//\n// The server accepts only SUBSCRIPTION operations on this socket \u2014 queries and\n// mutations go over HTTP \u2014 and it closes the socket with 4401 when the access token\n// it authenticated with expires. subscribe() handles that close itself: when a\n// socket that had been acknowledged is closed with 4401, it re-issues the operation\n// once, which opens a new socket whose connection_init carries a freshly resolved\n// token, and reports the reconnect to the sink as connected(true) so a consumer\n// re-queries whatever it may have missed. graphql-ws treats 4401 as fatal and does\n// not retry it on its own.\n\nimport type { DocumentTypeDecoration } from '@graphql-typed-document-node/core';\nimport { createClient, type Client } from 'graphql-ws';\n\nimport { areaPath, resolveAuthToken, type Area } from './transport';\n\n// The graphql-transport-ws close code for an unauthorized connection. Mid-session\n// it means the token the socket authenticated with has expired.\nconst CLOSE_UNAUTHORIZED = 4401;\n\n// wsUrl builds the absolute ws(s):// URL for an area from the current origin,\n// mirroring the relative http path so dev proxy and prod ingress both work.\nfunction wsUrl(area: Area): string {\n const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws';\n return `${scheme}://${window.location.host}${areaPath(area)}`;\n}\n\n// An area's shared graphql-ws client, plus what subscribe() needs to know about its\n// socket to decide whether a 4401 close is worth one re-subscribe.\ninterface AreaClient {\n client: Client;\n // The current socket has received connection_ack.\n acked: boolean;\n // The most recently closed socket had been acknowledged. A 4401 on an\n // acknowledged socket is an expiry mid-session, which a fresh token cures; a 4401\n // on one that was never acknowledged is the server refusing the token presented\n // at connect, which re-subscribing would only repeat.\n lastCloseWasAcked: boolean;\n}\n\n// One graphql-ws client per area \u2014 the WebSocket (and its connection_init auth)\n// is reused across every subscription to that area. connectionParams is a\n// function so the token is re-resolved on each (re)connect, surviving refresh.\nconst clients = new Map<Area, AreaClient>();\n\nfunction clientFor(area: Area): AreaClient {\n let entry = clients.get(area);\n if (!entry) {\n const client = createClient({\n url: wsUrl(area),\n connectionParams: async () => {\n const token = await resolveAuthToken();\n return token ? { Authorization: `Bearer ${token}` } : {};\n },\n });\n const state: AreaClient = { client, acked: false, lastCloseWasAcked: false };\n // Registered once, here, before any subscription can register its own \u2014 so on a\n // close this runs first and every sink's error handler (which graphql-ws delivers\n // later, through a rejected promise) reads the same lastCloseWasAcked. That holds\n // even after the first of them re-subscribes and a new socket starts connecting.\n client.on('connecting', () => {\n state.acked = false;\n });\n client.on('connected', () => {\n state.acked = true;\n });\n client.on('closed', () => {\n state.lastCloseWasAcked = state.acked;\n state.acked = false;\n });\n entry = state;\n clients.set(area, entry);\n }\n return entry;\n}\n\n// isUnauthorizedClose reports whether a graphql-ws sink error is a 4401 close event.\nfunction isUnauthorizedClose(err: unknown): boolean {\n return (\n typeof err === 'object' && err !== null && 'code' in err && (err as { code: unknown }).code === CLOSE_UNAUTHORIZED\n );\n}\n\nexport interface SubscriptionSink<T> {\n next: (data: T) => void;\n error?: (err: unknown) => void;\n complete?: () => void;\n // Connection-level signals for the shared per-area socket, distinct from the\n // per-operation next/error/complete above. `connected` fires on each successful\n // connection_ack \u2014 `wasRetry` distinguishes a reconnect (after a dropped socket,\n // or after the server closed an expired session) from the first connect; `closed`\n // fires on each socket close (graphql-ws then auto-retries a transient close). A\n // consumer that only needs data can omit both; a live-status indicator uses them\n // to tell \"connected but idle\" from \"offline\", and a consumer that must not miss\n // events re-queries on connected(true).\n connected?: (wasRetry: boolean) => void;\n closed?: () => void;\n}\n\n// subscribe binds a typed subscription document to a sink and returns an\n// unsubscribe function. The document is the same code-generated typed document\n// the query path uses, so the result type is inferred; it must be a subscription,\n// since the server refuses anything else on this socket. Errors from the operation\n// (or the socket) surface via sink.error; normal termination via sink.complete. A\n// 4401 close of an acknowledged socket \u2014 the server ending an expired session \u2014 is\n// not surfaced: the operation is re-issued once on a new socket with a fresh token.\nexport function subscribe<TResult, TVariables>(\n area: Area,\n document: DocumentTypeDecoration<TResult, TVariables> & { toString(): string },\n variables: TVariables extends Record<string, never> ? undefined : TVariables,\n sink: SubscriptionSink<TResult>,\n): () => void {\n const entry = clientFor(area);\n const { client } = entry;\n // Set when this operation is re-issued after an expiry, so the next connection_ack\n // reaches the sink as a reconnect: graphql-ws itself reports it as a first connect,\n // because a fatal close ends its retry state.\n let reauth = false;\n // Client-level connection events are registered per subscribe() and torn down with\n // it, alongside the operation itself, so a single dispose() call releases both.\n const disposers: Array<() => void> = [];\n if (sink.connected) {\n disposers.push(\n client.on('connected', (_socket, _payload, wasRetry) => {\n const retried = wasRetry || reauth;\n reauth = false;\n sink.connected!(retried);\n }),\n );\n }\n if (sink.closed) {\n disposers.push(client.on('closed', () => sink.closed!()));\n }\n\n const payload = {\n query: document.toString(),\n variables: variables as Record<string, unknown> | undefined,\n };\n let disposed = false;\n let current: (() => void) | null = null;\n const start = (): void => {\n const previous = current;\n current = client.subscribe<TResult>(payload, {\n next: (result) => {\n if (result.data != null) sink.next(result.data);\n },\n error: (err) => {\n if (disposed) return;\n if (isUnauthorizedClose(err) && entry.lastCloseWasAcked) {\n reauth = true;\n start();\n return;\n }\n sink.error?.(err);\n },\n complete: () => sink.complete?.(),\n });\n // Release the ended operation only once its replacement holds the client, so the\n // client's count of live operations never touches zero in between (graphql-ws\n // does not release an operation that ended in a fatal close by itself).\n previous?.();\n };\n start();\n disposers.push(() => {\n disposed = true;\n current?.();\n });\n return () => {\n for (const dispose of disposers) dispose();\n };\n}\n\n// disposeSubscriptions tears down every cached area client (their sockets). A\n// host calls this on sign-out so a stale token's socket does not linger.\nexport function disposeSubscriptions(): void {\n for (const { client } of clients.values()) {\n void client.dispose();\n }\n clients.clear();\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Client-side decode of the DeviceChain access token (ADR-008 JWT). This reads\n// the payload for display + routing only; it does NOT verify the signature \u2014\n// every protected operation is still enforced server-side by the token's\n// authorities. Never trust these claims for authorization decisions beyond what\n// the UI shows.\n\nexport interface DecodedClaims {\n /** Tenant the subject acts within (authoritative server-side). */\n tenant: string;\n /** Human-readable subject identifier. */\n username: string;\n /** Assigned role names \u2014 display/audit only; enforcement is on authorities. */\n roles: string[];\n /** Effective capabilities (e.g. \"device:write\", or \"*\" for super-authority). */\n authorities: string[];\n /** \"access\" | \"refresh\". */\n typ: string;\n /** Expiry, epoch seconds (from the standard `exp` claim). */\n exp?: number;\n}\n\nfunction base64UrlDecode(segment: string): string {\n const padded = segment.replace(/-/g, '+').replace(/_/g, '/');\n const json = atob(padded);\n // Handle UTF-8 payloads.\n return decodeURIComponent(\n Array.prototype.map\n .call(json, (c: string) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))\n .join(''),\n );\n}\n\n/** Decode a JWT's payload, or null if it is malformed. */\nexport function decodeToken(token: string): DecodedClaims | null {\n const parts = token.split('.');\n if (parts.length !== 3) return null;\n try {\n const payload = JSON.parse(base64UrlDecode(parts[1]));\n return {\n tenant: payload.tenant ?? '',\n username: payload.username ?? '',\n roles: payload.roles ?? [],\n authorities: payload.authorities ?? [],\n typ: payload.typ ?? '',\n exp: payload.exp,\n };\n } catch {\n return null;\n }\n}\n\n/** True if the token is missing, malformed, or within `skewSeconds` of expiry. */\nexport function isExpired(token: string, skewSeconds = 30): boolean {\n const claims = decodeToken(token);\n if (!claims || claims.exp === undefined) return true;\n const nowSeconds = Date.now() / 1000;\n return claims.exp - skewSeconds <= nowSeconds;\n}\n\n/** True if the claims grant the authority (the super-authority \"*\" passes all). */\nexport function hasAuthority(claims: DecodedClaims | null, required: string): boolean {\n if (!claims) return false;\n return claims.authorities.some((a) => a === '*' || a === required);\n}\n", "// Copyright The DeviceChain Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Token masks (ADR-042 P3): the client-side template language that mints, and\n// soft-validates, entity tokens. A mask is literal text plus typed placeholders,\n// and one mask drives three operations:\n//\n// generate fill placeholders to produce a concrete token\n// normalize kebab-case a human string into a {slug}\n// validate does a token conform to the mask's shape (soft, UI-only)\n//\n// Masks are advisory: the backend enforces only the security grammar\n// (isValidToken below mirrors rdb.ValidateToken). Style \u2014 lowercase-kebab,\n// prefixes \u2014 lives in the mask, applied here in the console, never on the wire.\n\n// The human-readable alphabet for generated ids: lowercase letters + digits minus\n// the ambiguous 0 o 1 l i, so a minted token can be read back off a label without\n// transcription errors.\nconst READABLE_ALPHABET = 'abcdefghjkmnpqrstuvwxyz23456789';\nconst DIGITS = '0123456789';\n\n// The security grammar the backend enforces (rdb.ValidateToken): letters (either\n// case), digits, hyphen and underscore, starting alphanumeric. Kept in sync by\n// hand \u2014 the same regex, mirrored for a pre-submit check so the user sees a bad\n// token before the server rejects it.\nconst TOKEN_GRAMMAR = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;\n\n/** The maximum token length (mirrors rdb.MaxTokenLen). */\nexport const MAX_TOKEN_LEN = 128;\n\n/** isValidToken mirrors the backend security grammar for a pre-submit check. */\nexport function isValidToken(token: string): boolean {\n return token.length > 0 && token.length <= MAX_TOKEN_LEN && TOKEN_GRAMMAR.test(token);\n}\n\n/** A parsed mask segment: a literal run, or a typed placeholder. */\nexport type MaskSegment =\n | { kind: 'literal'; text: string }\n | { kind: 'placeholder'; type: 'alphanumeric' | 'numeric' | 'slug' | 'uuid' | 'unknown'; n?: number; raw: string };\n\nconst PLACEHOLDER = /\\{([a-zA-Z]+)(?:-(\\d+))?\\}/g;\nconst KNOWN = ['alphanumeric', 'numeric', 'slug', 'uuid'] as const;\n\n/** parseMask splits a mask into its literal and placeholder segments. */\nexport function parseMask(mask: string): MaskSegment[] {\n const segments: MaskSegment[] = [];\n let last = 0;\n for (const m of mask.matchAll(PLACEHOLDER)) {\n const at = m.index ?? 0;\n if (at > last) segments.push({ kind: 'literal', text: mask.slice(last, at) });\n const name = m[1].toLowerCase();\n const type = (KNOWN as readonly string[]).includes(name) ? (name as 'alphanumeric') : 'unknown';\n segments.push({ kind: 'placeholder', type, n: m[2] ? parseInt(m[2], 10) : undefined, raw: m[0] });\n last = at + m[0].length;\n }\n if (last < mask.length) segments.push({ kind: 'literal', text: mask.slice(last) });\n return segments;\n}\n\nexport interface GenerateOptions {\n /** A human string (e.g. the entity's name) that fills {slug} placeholders. */\n seed?: string;\n /** Injectable RNG in [0,1) for deterministic tests; defaults to Math.random. */\n random?: () => number;\n /** Injectable uuid for deterministic tests; defaults to crypto.randomUUID. */\n uuid?: () => string;\n}\n\nfunction randomUUID(): string {\n const g = globalThis as { crypto?: { randomUUID?: () => string } };\n if (g.crypto?.randomUUID) return g.crypto.randomUUID();\n // Fallback for environments without Web Crypto (not the console; belt & braces).\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\n/**\n * generateToken fills a mask's placeholders to produce a concrete token:\n * {alphanumeric-N} \u2192 N readable chars, {numeric-N} \u2192 N digits, {uuid} \u2192 a uuid,\n * {slug} \u2192 the normalized seed (or a short readable id when no seed is given, so\n * a \"regenerate\" button always yields something). Literals pass through.\n */\nexport function generateToken(mask: string, opts: GenerateOptions = {}): string {\n const rand = opts.random ?? Math.random;\n const uuid = opts.uuid ?? randomUUID;\n // \uD83D\uDD34 The width is bounded before it is used. A mask may name any width the\n // regexp matches, and `Array.from({length: 1e20})` THROWS RangeError \u2014 which\n // made this function, called during render by the settings editor and by every\n // create form, a way to kill the page from a stored value. No width past\n // MAX_TOKEN_LEN can mint a legal token anyway, so an absurd one contributes\n // nothing rather than exploding. validateMask refuses such a mask outright;\n // this keeps generation safe for a mask stored before it did.\n const pick = (alphabet: string, n: number) =>\n n > MAX_TOKEN_LEN\n ? ''\n : Array.from({ length: n }, () => alphabet[Math.floor(rand() * alphabet.length)]).join('');\n\n return parseMask(mask)\n .map((seg) => {\n if (seg.kind === 'literal') return seg.text;\n switch (seg.type) {\n case 'alphanumeric':\n return pick(READABLE_ALPHABET, seg.n ?? 8);\n case 'numeric':\n return pick(DIGITS, seg.n ?? 4);\n case 'slug':\n return opts.seed ? normalizeToken(opts.seed) : pick(READABLE_ALPHABET, 6);\n case 'uuid':\n return uuid();\n default:\n return ''; // an unknown placeholder contributes nothing to a generated token\n }\n })\n .join('');\n}\n\n/**\n * normalizeToken kebab-cases a human string into a slug: lower-case, spaces and\n * underscores to hyphens, drop anything else, collapse and trim hyphens.\n * \"Ops Overview\" \u2192 \"ops-overview\".\n */\nexport function normalizeToken(input: string): string {\n return input\n .toLowerCase()\n .trim()\n .replace(/[\\s_]+/g, '-')\n .replace(/[^a-z0-9-]+/g, '')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * maskToRegExp compiles a mask into a whole-string RegExp for soft, UI-only\n * conformance checking. It is intentionally lenient (accepts either case in\n * alphanumeric/uuid segments) \u2014 the backend, not this, is the authority.\n */\nexport function maskToRegExp(mask: string): RegExp {\n const body = parseMask(mask)\n .map((seg) => {\n if (seg.kind === 'literal') return escapeRegExp(seg.text);\n switch (seg.type) {\n case 'alphanumeric':\n return `[A-Za-z0-9]{${seg.n ?? 8}}`;\n case 'numeric':\n return `[0-9]{${seg.n ?? 4}}`;\n case 'slug':\n return `[a-z0-9]+(?:-[a-z0-9]+)*`;\n case 'uuid':\n return `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`;\n default:\n return escapeRegExp(seg.raw); // unknown placeholder must match itself literally\n }\n })\n .join('');\n return new RegExp(`^${body}$`);\n}\n\n/** conformsToMask reports whether a token matches the mask's shape (soft check). */\nexport function conformsToMask(mask: string, token: string): boolean {\n return maskToRegExp(mask).test(token);\n}\n\n/** The fixed seed sampleToken fills {slug} placeholders from. */\nconst SAMPLE_SEED = 'Sample Name';\n\n/**\n * sampleToken generates the token a mask would mint, deterministically. Same\n * generator as generateToken \u2014 a pinned RNG and uuid rather than a second\n * implementation, so a sample can never disagree with what a create form\n * actually produces.\n */\nexport function sampleToken(mask: string, seed: string = SAMPLE_SEED): string {\n return generateToken(mask, {\n seed,\n random: () => 0,\n uuid: () => '3f2b8c14-9d7e-4a51-b6c2-0e8d5a1f7b93',\n });\n}\n\n/**\n * Why a mask cannot mint usable tokens. Structured rather than a message\n * because this package is consumed by localized apps \u2014 the caller maps a reason\n * to its own wording.\n */\nexport type MaskProblem =\n | { reason: 'empty' }\n | { reason: 'unknownPlaceholder'; placeholder: string }\n | { reason: 'noPlaceholder' }\n | { reason: 'widthTooLarge'; placeholder: string; max: number }\n | { reason: 'invalidToken'; sample: string };\n\n/**\n * validateMask reports why a mask is unusable, or null when it is fine. It\n * mirrors the server (the tokenmask Go package), which is the authority and\n * refuses the same four things \u2014 every one of which fails SILENTLY otherwise:\n *\n * empty nothing to mint from\n * unknownPlaceholder contributes nothing, so \"dev-{sulg}\" mints \"dev-\"\n * noPlaceholder every entity mints the SAME token; the second create collides\n * invalidToken the minted sample is not a legal token (a space, a slash\u2026)\n *\n * \uD83D\uDD34 Keep this NO STRICTER than the server. A check the server does not make\n * would refuse a mask the platform accepts, and the operator would have no way\n * to get past it.\n */\nexport function validateMask(mask: string): MaskProblem | null {\n if (mask === '') return { reason: 'empty' };\n let placeholders = 0;\n for (const seg of parseMask(mask)) {\n if (seg.kind !== 'placeholder') continue;\n if (seg.type === 'unknown') return { reason: 'unknownPlaceholder', placeholder: seg.raw };\n // \uD83D\uDD34 Checked BEFORE anything samples the mask. This function is called during\n // render, per keystroke \u2014 a width of 5e7 froze the tab for seconds building a\n // string nobody would ever see, and 1e20 threw RangeError and killed the\n // page. Neither reached the `invalidToken` check below, which is where a\n // reader would expect an over-long token to be caught.\n if (seg.n !== undefined && seg.n > MAX_TOKEN_LEN) {\n return { reason: 'widthTooLarge', placeholder: seg.raw, max: MAX_TOKEN_LEN };\n }\n placeholders++;\n }\n if (placeholders === 0) return { reason: 'noPlaceholder' };\n const sample = sampleToken(mask);\n if (!isValidToken(sample)) return { reason: 'invalidToken', sample };\n return null;\n}\n\n/**\n * resolveMask picks the mask for an entity type from a token-masks map, falling\n * back to the \"default\" entry and finally to a bare {slug}.\n */\nexport function resolveMask(masks: Record<string, string>, entityType: string): string {\n return masks[entityType] ?? masks.default ?? '{slug}';\n}\n"],
5
+ "mappings": ";AAyBO,IAAM,QAAQ;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQO,SAAS,SAAS,MAAoB;AAC3C,SAAO,QAAQ,IAAI;AACrB;AAeO,SAAS,QAAQ,MAAoB;AAC1C,SAAO,KAAK,MAAM,GAAG,EAAE,CAAC;AAC1B;AASO,SAAS,eAAe,MAAqB;AAClD,SAAO,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,WAAW;AAC7D;AAQA,IAAI,cAAqD;AAElD,SAAS,mBAAmB,QAA+C;AAChF,gBAAc;AAChB;AAKA,IAAI,sBAA6D;AAE1D,SAAS,uBAAuB,QAA+C;AACpF,wBAAsB;AACxB;AAKA,eAAsB,mBAA2C;AAC/D,SAAO,cAAc,YAAY,IAAI;AACvC;AAIO,IAAM,sBAAN,cAAkC,MAAM;AAAA,EAC7C,YACE,SAEO,QAEA,QACP;AACA,UAAM,OAAO;AAJN;AAEA;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AAkDA,eAAsB,IACpB,MACA,aACG,CAAC,WAAW,OAAO,GAGJ;AAClB,QAAM,UAAkC,EAAE,gBAAgB,mBAAmB;AAE7E,MAAI,CAAC,SAAS,WAAW;AACvB,UAAM,cAAc,SAAS,YAAY,eAAe,IAAI;AAC5D,UAAM,SAAS,cAAc,sBAAsB;AACnD,QAAI,QAAQ;AACV,YAAM,QAAQ,MAAM,OAAO;AAC3B,UAAI,MAAO,SAAQ,eAAe,IAAI,UAAU,KAAK;AAAA,IACvD;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,IAAI,GAAG;AAAA,MAChC,QAAQ;AAAA,MACR;AAAA,MACA,MAAM,KAAK,UAAU,EAAE,OAAO,SAAS,SAAS,GAAG,UAAU,CAAC;AAAA,IAChE,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,eAAe,QAAQ,IAAI,UAAU;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,oBAAoB,mBAAmB,IAAI,MAAM,KAAK,IAAI,MAAM;AAAA,EAC5E;AAEA,QAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,MAAI,KAAK,UAAU,KAAK,OAAO,SAAS,GAAG;AACzC,UAAM,IAAI,oBAAoB,KAAK,OAAO,CAAC,EAAE,SAAS,IAAI,QAAQ,KAAK,MAAM;AAAA,EAC/E;AACA,MAAI,KAAK,SAAS,QAAW;AAC3B,UAAM,IAAI,oBAAoB,0BAA0B,IAAI,MAAM;AAAA,EACpE;AACA,SAAO,KAAK;AACd;;;AC7LA,IAAM,mBAAmB;AAOlB,SAAS,iBAAiB,KAAuB;AACtD,MAAI,EAAE,eAAe,qBAAsB,QAAO;AAElD,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,QAAM,WAAW,IAAI,QAAQ,SAAS,IAAI,OAAO,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,IAAI,OAAO;AACrF,SAAO,SAAS,KAAK,CAAC,MAAM,iBAAiB,KAAK,EAAE,KAAK,CAAC,CAAC;AAC7D;;;ACFA,SAAS,KAAK,GAA6C;AACzD,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,QAAM,UAAU,EAAE,KAAK;AACvB,SAAO,YAAY,KAAK,OAAO;AACjC;AAEA,SAAS,IAAI,GAA6C;AACxD,SAAO,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,IAAI,IAAI;AAC3D;AAeO,SAAS,eAAe,UAAsC,QAA6C;AAChH,QAAM,IAAI,YAAY,CAAC;AACvB,QAAM,IAAI,UAAU,CAAC;AAsBrB,QAAM,sBAAsB,KAAK,EAAE,WAAW;AAC9C,QAAM,gBAAgB,wBAAwB,OAAO,OAAO,KAAK,EAAE,OAAO;AAC1E,QAAM,UAAU,iBAAiB,KAAK,EAAE,OAAO;AAC/C,QAAM,cAAc,gBAAgB,sBAAsB,KAAK,EAAE,WAAW;AAI5E,QAAM,oBAAoB,IAAI,EAAE,SAAS,MAAM,QAAQ,IAAI,EAAE,SAAS,MAAM;AAC5E,QAAM,YAAY,oBAAoB,IAAI,EAAE,SAAS,IAAI,IAAI,EAAE,SAAS;AACxE,QAAM,YAAY,oBAAoB,IAAI,EAAE,SAAS,IAAI,IAAI,EAAE,SAAS;AAExE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI;AAAA,EACjC;AACF;AASO,SAAS,kBAAkB,GAAuD;AACvF,QAAM,UAAU,KAAK,GAAG,OAAO;AAC/B,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO;AAAA,IACL;AAAA,IACA,aAAa,KAAK,GAAG,WAAW;AAAA,IAChC,WAAW,IAAI,GAAG,SAAS;AAAA,IAC3B,WAAW,IAAI,GAAG,SAAS;AAAA,IAC3B,MAAM,IAAI,GAAG,IAAI;AAAA,EACnB;AACF;AAUO,SAAS,aAAa,GAAyF;AACpH,QAAM,MAAM,IAAI,GAAG,SAAS;AAC5B,QAAM,MAAM,IAAI,GAAG,SAAS;AAC5B,MAAI,QAAQ,QAAQ,QAAQ,KAAM,QAAO;AACzC,SAAO,EAAE,QAAQ,CAAC,KAAK,GAAG,GAAG,MAAM,IAAI,GAAG,IAAI,EAAE;AAClD;;;AC9GA,SAAS,oBAAiC;AAM1C,IAAM,qBAAqB;AAI3B,SAAS,MAAM,MAAoB;AACjC,QAAM,SAAS,OAAO,SAAS,aAAa,WAAW,QAAQ;AAC/D,SAAO,GAAG,MAAM,MAAM,OAAO,SAAS,IAAI,GAAG,SAAS,IAAI,CAAC;AAC7D;AAkBA,IAAM,UAAU,oBAAI,IAAsB;AAE1C,SAAS,UAAU,MAAwB;AACzC,MAAI,QAAQ,QAAQ,IAAI,IAAI;AAC5B,MAAI,CAAC,OAAO;AACV,UAAM,SAAS,aAAa;AAAA,MAC1B,KAAK,MAAM,IAAI;AAAA,MACf,kBAAkB,YAAY;AAC5B,cAAM,QAAQ,MAAM,iBAAiB;AACrC,eAAO,QAAQ,EAAE,eAAe,UAAU,KAAK,GAAG,IAAI,CAAC;AAAA,MACzD;AAAA,IACF,CAAC;AACD,UAAM,QAAoB,EAAE,QAAQ,OAAO,OAAO,mBAAmB,MAAM;AAK3E,WAAO,GAAG,cAAc,MAAM;AAC5B,YAAM,QAAQ;AAAA,IAChB,CAAC;AACD,WAAO,GAAG,aAAa,MAAM;AAC3B,YAAM,QAAQ;AAAA,IAChB,CAAC;AACD,WAAO,GAAG,UAAU,MAAM;AACxB,YAAM,oBAAoB,MAAM;AAChC,YAAM,QAAQ;AAAA,IAChB,CAAC;AACD,YAAQ;AACR,YAAQ,IAAI,MAAM,KAAK;AAAA,EACzB;AACA,SAAO;AACT;AAGA,SAAS,oBAAoB,KAAuB;AAClD,SACE,OAAO,QAAQ,YAAY,QAAQ,QAAQ,UAAU,OAAQ,IAA0B,SAAS;AAEpG;AAyBO,SAAS,UACd,MACA,UACA,WACA,MACY;AACZ,QAAM,QAAQ,UAAU,IAAI;AAC5B,QAAM,EAAE,OAAO,IAAI;AAInB,MAAI,SAAS;AAGb,QAAM,YAA+B,CAAC;AACtC,MAAI,KAAK,WAAW;AAClB,cAAU;AAAA,MACR,OAAO,GAAG,aAAa,CAAC,SAAS,UAAU,aAAa;AACtD,cAAM,UAAU,YAAY;AAC5B,iBAAS;AACT,aAAK,UAAW,OAAO;AAAA,MACzB,CAAC;AAAA,IACH;AAAA,EACF;AACA,MAAI,KAAK,QAAQ;AACf,cAAU,KAAK,OAAO,GAAG,UAAU,MAAM,KAAK,OAAQ,CAAC,CAAC;AAAA,EAC1D;AAEA,QAAM,UAAU;AAAA,IACd,OAAO,SAAS,SAAS;AAAA,IACzB;AAAA,EACF;AACA,MAAI,WAAW;AACf,MAAI,UAA+B;AACnC,QAAM,QAAQ,MAAY;AACxB,UAAM,WAAW;AACjB,cAAU,OAAO,UAAmB,SAAS;AAAA,MAC3C,MAAM,CAAC,WAAW;AAChB,YAAI,OAAO,QAAQ,KAAM,MAAK,KAAK,OAAO,IAAI;AAAA,MAChD;AAAA,MACA,OAAO,CAAC,QAAQ;AACd,YAAI,SAAU;AACd,YAAI,oBAAoB,GAAG,KAAK,MAAM,mBAAmB;AACvD,mBAAS;AACT,gBAAM;AACN;AAAA,QACF;AACA,aAAK,QAAQ,GAAG;AAAA,MAClB;AAAA,MACA,UAAU,MAAM,KAAK,WAAW;AAAA,IAClC,CAAC;AAID,eAAW;AAAA,EACb;AACA,QAAM;AACN,YAAU,KAAK,MAAM;AACnB,eAAW;AACX,cAAU;AAAA,EACZ,CAAC;AACD,SAAO,MAAM;AACX,eAAW,WAAW,UAAW,SAAQ;AAAA,EAC3C;AACF;AAIO,SAAS,uBAA6B;AAC3C,aAAW,EAAE,OAAO,KAAK,QAAQ,OAAO,GAAG;AACzC,SAAK,OAAO,QAAQ;AAAA,EACtB;AACA,UAAQ,MAAM;AAChB;;;ACtKA,SAAS,gBAAgB,SAAyB;AAChD,QAAM,SAAS,QAAQ,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AAC3D,QAAM,OAAO,KAAK,MAAM;AAExB,SAAO;AAAA,IACL,MAAM,UAAU,IACb,KAAK,MAAM,CAAC,MAAc,OAAO,OAAO,EAAE,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAAC,EAC/E,KAAK,EAAE;AAAA,EACZ;AACF;AAGO,SAAS,YAAY,OAAqC;AAC/D,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,MAAI;AACF,UAAM,UAAU,KAAK,MAAM,gBAAgB,MAAM,CAAC,CAAC,CAAC;AACpD,WAAO;AAAA,MACL,QAAQ,QAAQ,UAAU;AAAA,MAC1B,UAAU,QAAQ,YAAY;AAAA,MAC9B,OAAO,QAAQ,SAAS,CAAC;AAAA,MACzB,aAAa,QAAQ,eAAe,CAAC;AAAA,MACrC,KAAK,QAAQ,OAAO;AAAA,MACpB,KAAK,QAAQ;AAAA,IACf;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,UAAU,OAAe,cAAc,IAAa;AAClE,QAAM,SAAS,YAAY,KAAK;AAChC,MAAI,CAAC,UAAU,OAAO,QAAQ,OAAW,QAAO;AAChD,QAAM,aAAa,KAAK,IAAI,IAAI;AAChC,SAAO,OAAO,MAAM,eAAe;AACrC;AAGO,SAAS,aAAa,QAA8B,UAA2B;AACpF,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,OAAO,YAAY,KAAK,CAAC,MAAM,MAAM,OAAO,MAAM,QAAQ;AACnE;;;AChDA,IAAM,oBAAoB;AAC1B,IAAM,SAAS;AAMf,IAAM,gBAAgB;AAGf,IAAM,gBAAgB;AAGtB,SAAS,aAAa,OAAwB;AACnD,SAAO,MAAM,SAAS,KAAK,MAAM,UAAU,iBAAiB,cAAc,KAAK,KAAK;AACtF;AAOA,IAAM,cAAc;AACpB,IAAM,QAAQ,CAAC,gBAAgB,WAAW,QAAQ,MAAM;AAGjD,SAAS,UAAU,MAA6B;AACrD,QAAM,WAA0B,CAAC;AACjC,MAAI,OAAO;AACX,aAAW,KAAK,KAAK,SAAS,WAAW,GAAG;AAC1C,UAAM,KAAK,EAAE,SAAS;AACtB,QAAI,KAAK,KAAM,UAAS,KAAK,EAAE,MAAM,WAAW,MAAM,KAAK,MAAM,MAAM,EAAE,EAAE,CAAC;AAC5E,UAAM,OAAO,EAAE,CAAC,EAAE,YAAY;AAC9B,UAAM,OAAQ,MAA4B,SAAS,IAAI,IAAK,OAA0B;AACtF,aAAS,KAAK,EAAE,MAAM,eAAe,MAAM,GAAG,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC,GAAG,EAAE,IAAI,QAAW,KAAK,EAAE,CAAC,EAAE,CAAC;AAChG,WAAO,KAAK,EAAE,CAAC,EAAE;AAAA,EACnB;AACA,MAAI,OAAO,KAAK,OAAQ,UAAS,KAAK,EAAE,MAAM,WAAW,MAAM,KAAK,MAAM,IAAI,EAAE,CAAC;AACjF,SAAO;AACT;AAWA,SAAS,aAAqB;AAC5B,QAAM,IAAI;AACV,MAAI,EAAE,QAAQ,WAAY,QAAO,EAAE,OAAO,WAAW;AAErD,SAAO,uCAAuC,QAAQ,SAAS,CAAC,MAAM;AACpE,UAAM,IAAK,KAAK,OAAO,IAAI,KAAM;AACjC,YAAQ,MAAM,MAAM,IAAK,IAAI,IAAO,GAAK,SAAS,EAAE;AAAA,EACtD,CAAC;AACH;AAQO,SAAS,cAAc,MAAc,OAAwB,CAAC,GAAW;AAC9E,QAAM,OAAO,KAAK,UAAU,KAAK;AACjC,QAAM,OAAO,KAAK,QAAQ;AAQ1B,QAAM,OAAO,CAAC,UAAkB,MAC9B,IAAI,gBACA,KACA,MAAM,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,SAAS,KAAK,MAAM,KAAK,IAAI,SAAS,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE;AAE7F,SAAO,UAAU,IAAI,EAClB,IAAI,CAAC,QAAQ;AACZ,QAAI,IAAI,SAAS,UAAW,QAAO,IAAI;AACvC,YAAQ,IAAI,MAAM;AAAA,MAChB,KAAK;AACH,eAAO,KAAK,mBAAmB,IAAI,KAAK,CAAC;AAAA,MAC3C,KAAK;AACH,eAAO,KAAK,QAAQ,IAAI,KAAK,CAAC;AAAA,MAChC,KAAK;AACH,eAAO,KAAK,OAAO,eAAe,KAAK,IAAI,IAAI,KAAK,mBAAmB,CAAC;AAAA,MAC1E,KAAK;AACH,eAAO,KAAK;AAAA,MACd;AACE,eAAO;AAAA,IACX;AAAA,EACF,CAAC,EACA,KAAK,EAAE;AACZ;AAOO,SAAS,eAAe,OAAuB;AACpD,SAAO,MACJ,YAAY,EACZ,KAAK,EACL,QAAQ,WAAW,GAAG,EACtB,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,OAAO,GAAG,EAClB,QAAQ,YAAY,EAAE;AAC3B;AAEA,SAAS,aAAa,GAAmB;AACvC,SAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;AAOO,SAAS,aAAa,MAAsB;AACjD,QAAM,OAAO,UAAU,IAAI,EACxB,IAAI,CAAC,QAAQ;AACZ,QAAI,IAAI,SAAS,UAAW,QAAO,aAAa,IAAI,IAAI;AACxD,YAAQ,IAAI,MAAM;AAAA,MAChB,KAAK;AACH,eAAO,eAAe,IAAI,KAAK,CAAC;AAAA,MAClC,KAAK;AACH,eAAO,SAAS,IAAI,KAAK,CAAC;AAAA,MAC5B,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT;AACE,eAAO,aAAa,IAAI,GAAG;AAAA,IAC/B;AAAA,EACF,CAAC,EACA,KAAK,EAAE;AACV,SAAO,IAAI,OAAO,IAAI,IAAI,GAAG;AAC/B;AAGO,SAAS,eAAe,MAAc,OAAwB;AACnE,SAAO,aAAa,IAAI,EAAE,KAAK,KAAK;AACtC;AAGA,IAAM,cAAc;AAQb,SAAS,YAAY,MAAc,OAAe,aAAqB;AAC5E,SAAO,cAAc,MAAM;AAAA,IACzB;AAAA,IACA,QAAQ,MAAM;AAAA,IACd,MAAM,MAAM;AAAA,EACd,CAAC;AACH;AA4BO,SAAS,aAAa,MAAkC;AAC7D,MAAI,SAAS,GAAI,QAAO,EAAE,QAAQ,QAAQ;AAC1C,MAAI,eAAe;AACnB,aAAW,OAAO,UAAU,IAAI,GAAG;AACjC,QAAI,IAAI,SAAS,cAAe;AAChC,QAAI,IAAI,SAAS,UAAW,QAAO,EAAE,QAAQ,sBAAsB,aAAa,IAAI,IAAI;AAMxF,QAAI,IAAI,MAAM,UAAa,IAAI,IAAI,eAAe;AAChD,aAAO,EAAE,QAAQ,iBAAiB,aAAa,IAAI,KAAK,KAAK,cAAc;AAAA,IAC7E;AACA;AAAA,EACF;AACA,MAAI,iBAAiB,EAAG,QAAO,EAAE,QAAQ,gBAAgB;AACzD,QAAM,SAAS,YAAY,IAAI;AAC/B,MAAI,CAAC,aAAa,MAAM,EAAG,QAAO,EAAE,QAAQ,gBAAgB,OAAO;AACnE,SAAO;AACT;AAMO,SAAS,YAAY,OAA+B,YAA4B;AACrF,SAAO,MAAM,UAAU,KAAK,MAAM,WAAW;AAC/C;",
6
6
  "names": []
7
7
  }
@@ -24,16 +24,22 @@ export declare class GraphQLRequestError extends Error {
24
24
  /** HTTP status, or 0 for a transport-level failure. */
25
25
  status: number;
26
26
  /** The raw `errors` array from a GraphQL response, when present. */
27
- errors?: {
28
- message: string;
29
- }[] | undefined;
27
+ errors?: GraphQLErrorEntry[] | undefined;
30
28
  constructor(message: string,
31
29
  /** HTTP status, or 0 for a transport-level failure. */
32
30
  status: number,
33
31
  /** The raw `errors` array from a GraphQL response, when present. */
34
- errors?: {
35
- message: string;
36
- }[] | undefined);
32
+ errors?: GraphQLErrorEntry[] | undefined);
33
+ }
34
+ /**
35
+ * One entry of a GraphQL response's `errors` array. `extensions` carries the server's
36
+ * machine-readable detail when it sends any — for example `code: "THROTTLED"` with
37
+ * `retryAfterSeconds` on a sign-in refused by the per-account backoff — so a caller can
38
+ * branch on a code instead of matching the message text.
39
+ */
40
+ export interface GraphQLErrorEntry {
41
+ message: string;
42
+ extensions?: Record<string, unknown>;
37
43
  }
38
44
  export interface RequestOptions {
39
45
  /** Skip attaching the Bearer token (login / refresh run unauthenticated). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devicechain/client",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "DeviceChain client SDK — two-tier auth, typed GraphQL operations, and graphql-ws subscriptions over the client wire.",
5
5
  "keywords": [
6
6
  "devicechain",