@versionone/agility-sdk 1.0.0-alpha.3 → 1.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -117,10 +117,22 @@ function messageOf(data, status) {
117
117
  return `HTTP ${status}`;
118
118
  }
119
119
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
120
+ var MAX_RETRY_AFTER_MS = 3e4;
121
+ function retryDelay(response, attempt) {
122
+ const header = response.headers.get("retry-after");
123
+ if (header) {
124
+ const seconds = Number(header);
125
+ const ms = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(header) - Date.now();
126
+ if (Number.isFinite(ms) && ms > MAX_RETRY_AFTER_MS) return null;
127
+ if (Number.isFinite(ms) && ms > 0) return ms;
128
+ }
129
+ return 150 * (attempt + 1);
130
+ }
131
+ var retriable = (status) => status === 429 || status >= 500;
120
132
  function createFetchTransport(config) {
121
133
  const fetchImpl = config.fetchImpl ?? fetch;
122
134
  const timeoutMs = config.timeoutMs ?? 6e4;
123
- const retries = Math.max(0, config.retries ?? 0);
135
+ const retries = Math.max(0, config.retries ?? 1);
124
136
  const authorization = config.authorization ?? (config.accessToken ? `Bearer ${config.accessToken}` : void 0);
125
137
  const unauthorizedStatuses = config.unauthorizedStatuses ?? [401];
126
138
  return {
@@ -163,6 +175,10 @@ function createFetchTransport(config) {
163
175
  options.onResponse?.({ status: response.status, headers: response.headers });
164
176
  return await readBody(response, responseType);
165
177
  }
178
+ if (response.status === 304 && options.acceptNotModified) {
179
+ options.onResponse?.({ status: 304, headers: response.headers });
180
+ return void 0;
181
+ }
166
182
  const data = await readBody(response, responseType === "bytes" ? "text" : responseType === "json" ? "auto" : responseType);
167
183
  lastError = new ApiError(messageOf(data, response.status), response.status, void 0, data);
168
184
  } catch (error) {
@@ -181,8 +197,10 @@ function createFetchTransport(config) {
181
197
  }
182
198
  break;
183
199
  }
184
- if (response.status >= 500 && attempt + 1 < attempts) {
185
- await delay(150 * (attempt + 1));
200
+ if (retriable(response.status) && attempt + 1 < attempts) {
201
+ const wait = retryDelay(response, attempt);
202
+ if (wait === null) break;
203
+ await delay(wait);
186
204
  continue;
187
205
  }
188
206
  break;
@@ -389,24 +407,29 @@ function toNumber(value) {
389
407
  const n = typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
390
408
  return Number.isNaN(n) ? value : n;
391
409
  }
410
+ function failureDetail(index, message, echoed) {
411
+ const detail = { index, message, violation: parseViolation(message) };
412
+ Object.defineProperty(detail, "echoed", { value: echoed, enumerable: false, writable: false });
413
+ return detail;
414
+ }
392
415
  function parseViolation(message) {
393
416
  const parts = message.trim().split("'");
394
417
  if (parts[0] !== "Violation" || !parts[1]) return null;
395
418
  const rule = parts[1], rest = parts.slice(2).filter((p) => p !== "");
396
419
  if (rest[0] === "AttributeDefinition" && rest[1] && rest[2]) return { rule, attribute: `${rest[2]}.${rest[1]}`, tag: rest.join("'") };
397
420
  const token = rest.length === 1 && /^[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9_]*$/.test(rest[0]) ? rest[0] : null;
398
- return { rule, attribute: token, tag: rest.length ? rest.join("'") : null };
421
+ return { rule, attribute: token, tag: !token && rest.length > 1 ? rest.join("'") : null };
399
422
  }
400
423
  function failuresOf(response) {
401
- return (response.commandFailures?.commands ?? []).map(({ error, ...echoed }) => ({
402
- index: typeof error === "object" && error && typeof error.sourceCommandIndex === "number" ? error.sourceCommandIndex : null,
403
- message: typeof error === "string" ? error : error?.message || "command failed",
404
- echoed,
405
- violation: parseViolation(typeof error === "string" ? error : error?.message || "")
406
- }));
424
+ return (response.commandFailures?.commands ?? []).map(({ error, ...echoed }) => failureDetail(
425
+ typeof error === "object" && error && typeof error.sourceCommandIndex === "number" ? error.sourceCommandIndex : null,
426
+ typeof error === "string" ? error : error?.message || "command failed",
427
+ echoed
428
+ ));
407
429
  }
408
430
  function outcomeOf(response) {
409
431
  return {
432
+ requestId: typeof response.requestId === "string" && response.requestId ? response.requestId : null,
410
433
  created: (response.assetsCreated?.oidTokens ?? []).map(momentless),
411
434
  modified: (response.assetsModified?.oidTokens ?? []).map(momentless),
412
435
  operated: (response.assetsOperatedOn?.oidTokens ?? []).map(momentless),
@@ -415,13 +438,14 @@ function outcomeOf(response) {
415
438
  durationSeconds: typeof response.durationSeconds === "number" ? response.durationSeconds : null
416
439
  };
417
440
  }
418
- var emptyOutcome = () => ({ created: [], modified: [], operated: [], failures: [], queryResults: [], durationSeconds: null });
441
+ var emptyOutcome = () => ({ requestId: null, created: [], modified: [], operated: [], failures: [], queryResults: [], durationSeconds: null });
419
442
  function mergeOutcome(into, done, offset) {
420
443
  into.created.push(...done.created);
421
444
  into.modified.push(...done.modified);
422
445
  into.operated.push(...done.operated);
423
- into.failures.push(...done.failures.map((f) => ({ ...f, index: f.index === null ? null : f.index + offset })));
446
+ into.failures.push(...done.failures.map((f) => failureDetail(f.index === null ? null : f.index + offset, f.message, f.echoed)));
424
447
  into.queryResults.push(...done.queryResults);
448
+ into.requestId = null;
425
449
  if (done.durationSeconds !== null) into.durationSeconds = (into.durationSeconds ?? 0) + done.durationSeconds;
426
450
  return into;
427
451
  }
@@ -1102,6 +1126,65 @@ async function reversible(transport, receipt, options = {}) {
1102
1126
  return { commands, drifted, reactivated };
1103
1127
  }
1104
1128
 
1129
+ // src/wire/fields.ts
1130
+ var fields_exports = {};
1131
+ __export(fields_exports, {
1132
+ visibleFields: () => visibleFields,
1133
+ visibleFieldsByAsset: () => visibleFieldsByAsset
1134
+ });
1135
+ async function visibleFields(transport, types, scopes) {
1136
+ if (!types.length) return {};
1137
+ const params = [...scopes.map((s) => `scopeOid=${encodeURIComponent(momentless(s))}`), ...types.map((t) => `assetTypeToken=${encodeURIComponent(t)}`)].join("&");
1138
+ const answer = await transport.request({ method: "GET", path: `/api/visibleFields?${params}`, responseType: "json" });
1139
+ if (!Array.isArray(answer)) throw new ApiError("api/visibleFields did not answer a list", 200, "BAD_RESPONSE", answer);
1140
+ const out = {};
1141
+ for (const entry of answer) if (entry && typeof entry.assetType === "string") out[entry.assetType] = Array.isArray(entry.fields) ? entry.fields.filter((f) => typeof f === "string") : [];
1142
+ return out;
1143
+ }
1144
+ async function visibleFieldsByAsset(transport, oid) {
1145
+ const answer = await transport.request({ method: "GET", path: "/api/visibleFieldsByAsset", query: { oidToken: momentless(oid) }, responseType: "json" });
1146
+ if (!Array.isArray(answer)) throw new ApiError("api/visibleFieldsByAsset did not answer a list", 200, "BAD_RESPONSE", answer);
1147
+ return answer.filter((f) => typeof f === "string");
1148
+ }
1149
+
1150
+ // src/services/fields.ts
1151
+ async function lineage(schema, type) {
1152
+ const out = [];
1153
+ for (let t = type; t && !out.includes(t); ) {
1154
+ out.push(t);
1155
+ const def = await schema.load(t);
1156
+ t = def?.Base;
1157
+ }
1158
+ return out;
1159
+ }
1160
+ function importantName(token, types) {
1161
+ const dot = token.indexOf(".");
1162
+ if (dot < 0) return token;
1163
+ return types.includes(token.slice(0, dot)) ? token.slice(dot + 1) : null;
1164
+ }
1165
+ async function describe2(transport, schema, type, scope) {
1166
+ const scopeOid = momentless(scope);
1167
+ const types = await lineage(schema, type);
1168
+ const definition = await schema.load(type);
1169
+ const required = definition ? Object.values(definition.Attributes).filter((a) => a.IsRequired && !a.IsReadOnly).map((a) => a.Name).sort() : [];
1170
+ const [visible, [row]] = await Promise.all([
1171
+ visibleFields(transport, [type], [scopeOid]),
1172
+ query(transport, { from: "Scope", select: [{ from: "Scheme", select: ["ImportantFields", "IsRequiredAtClose"] }], filter: ["ID=$scope"], with: { $scope: scopeOid }, page: { size: 1, start: 0 } }, { lookup: schema })
1173
+ ]);
1174
+ const scheme = row?.Scheme ?? null;
1175
+ const tokens = Array.isArray(scheme?.ImportantFields) ? scheme.ImportantFields.filter((t) => typeof t === "string") : [];
1176
+ const important = [...new Set(tokens.map((t) => importantName(t, types)).filter((n) => !!n))].sort();
1177
+ return {
1178
+ type,
1179
+ scope: scopeOid,
1180
+ scheme: scheme ? momentless(scheme._oid) : null,
1181
+ required,
1182
+ visible: visible[type] ?? [],
1183
+ important,
1184
+ requiredAtClose: scheme?.IsRequiredAtClose === true
1185
+ };
1186
+ }
1187
+
1105
1188
  // src/types/asset.ts
1106
1189
  var ASSET_STATE_CODES = { Future: 0, Active: 64, Closed: 128, Dead: 192, Template: 200, OldBrokenDownStory: 208, Deprecated: 254, Deleted: 255 };
1107
1190
  var STATE_OF_CODE = Object.fromEntries(Object.entries(ASSET_STATE_CODES).map(([name, code]) => [code, name]));
@@ -1194,10 +1277,13 @@ async function resolveRef(transport, spec, options = {}) {
1194
1277
  // src/wire/meta.ts
1195
1278
  var meta_exports = {};
1196
1279
  __export(meta_exports, {
1280
+ etag: () => etag,
1197
1281
  getAssetType: () => getAssetType,
1198
1282
  getAttributeDefinition: () => getAttributeDefinition,
1199
1283
  getMetamodel: () => getMetamodel,
1200
1284
  isMetamodelDocument: () => isMetamodelDocument,
1285
+ readAssetType: () => readAssetType,
1286
+ readMetamodel: () => readMetamodel,
1201
1287
  toAssetType: () => toAssetType,
1202
1288
  toAttributeDefinition: () => toAttributeDefinition,
1203
1289
  toMetamodel: () => toMetamodel
@@ -1242,12 +1328,36 @@ function toMetamodel(doc) {
1242
1328
  function isMetamodelDocument(value) {
1243
1329
  return !!value && typeof value === "object" && typeof value.Version === "string" && !!value.AssetTypes && typeof value.AssetTypes === "object";
1244
1330
  }
1331
+ var PROBE_PATH = "/meta.v1/BaseAsset/Name";
1332
+ async function conditional(transport, path, options) {
1333
+ let etag2 = null, status = 200;
1334
+ const onResponse = (r) => {
1335
+ status = r.status;
1336
+ etag2 = r.headers.get("etag");
1337
+ };
1338
+ const headers = options.ifNoneMatch ? { "if-none-match": options.ifNoneMatch } : void 0;
1339
+ const doc = await transport.request({ method: "GET", path, headers, responseType: "json", onResponse, acceptNotModified: !!options.ifNoneMatch });
1340
+ if (status === 304 || doc === void 0) return null;
1341
+ return { value: doc, etag: etag2 };
1342
+ }
1245
1343
  async function getMetamodel(transport) {
1246
1344
  return toMetamodel(await transport.request({ method: "GET", path: "/meta.v1", responseType: "json" }));
1247
1345
  }
1346
+ async function readMetamodel(transport, options = {}) {
1347
+ const read2 = await conditional(transport, "/meta.v1", options);
1348
+ return read2 && { value: toMetamodel(read2.value), etag: read2.etag };
1349
+ }
1248
1350
  async function getAssetType(transport, token) {
1249
1351
  return toAssetType(await transport.request({ method: "GET", path: `/meta.v1/${encodeURIComponent(token)}`, responseType: "json" }));
1250
1352
  }
1353
+ async function readAssetType(transport, token, options = {}) {
1354
+ const read2 = await conditional(transport, `/meta.v1/${encodeURIComponent(token)}`, options);
1355
+ return read2 && { value: toAssetType(read2.value), etag: read2.etag };
1356
+ }
1357
+ async function etag(transport, options = {}) {
1358
+ const read2 = await conditional(transport, PROBE_PATH, options);
1359
+ return read2 ? read2.etag : options.ifNoneMatch ?? null;
1360
+ }
1251
1361
  async function getAttributeDefinition(transport, tokenref) {
1252
1362
  const [type, attribute] = tokenref.split(".");
1253
1363
  if (!type || !attribute) throw new UsageError(`"${tokenref}" is not an attribute token ("Story.Name")`);
@@ -1259,6 +1369,13 @@ var TypeCache = class _TypeCache {
1259
1369
  /** Loaded types; null records that the instance has no such type, so it is not asked for again. */
1260
1370
  types = /* @__PURE__ */ new Map();
1261
1371
  loading = /* @__PURE__ */ new Map();
1372
+ /** The schema's tag the loaded types came with (meta.v1's ETag, one for the whole schema); null until a load, or for a seeded cache. */
1373
+ etag = null;
1374
+ /** Forget every loaded type (and the tag): the next query loads afresh. In-flight loads finish into the cache as before. */
1375
+ clear() {
1376
+ this.types.clear();
1377
+ this.etag = null;
1378
+ }
1262
1379
  /** A cache seeded from a metamodel document. */
1263
1380
  static fromMetamodel(metamodel) {
1264
1381
  const cache = new _TypeCache();
@@ -1323,7 +1440,10 @@ var Schema = class _Schema {
1323
1440
  this.types.set(token, null);
1324
1441
  return null;
1325
1442
  }
1326
- const request = getAssetType(this.transport, token).catch((error) => {
1443
+ const request = readAssetType(this.transport, token).then((read2) => {
1444
+ if (read2?.etag) this.cache.etag = read2.etag;
1445
+ return read2 ? read2.value : null;
1446
+ }).catch((error) => {
1327
1447
  if (error instanceof ApiError && error.status === 404) return null;
1328
1448
  throw error;
1329
1449
  }).then((type) => {
@@ -1333,6 +1453,23 @@ var Schema = class _Schema {
1333
1453
  this.loading.set(token, request);
1334
1454
  return request;
1335
1455
  }
1456
+ /**
1457
+ * Has the instance's schema changed since these types were loaded? One
1458
+ * small conditional read of meta.v1. Unchanged: nothing happens.
1459
+ * Changed (or never tagged, or the transport cannot see headers): the
1460
+ * loaded types are dropped, so the next query loads them again, and the
1461
+ * new tag is kept. Answers whether it changed and the current tag. A
1462
+ * schema without a transport (seeded) never changes.
1463
+ */
1464
+ async revalidate() {
1465
+ if (!this.transport) return { changed: false, etag: this.cache.etag };
1466
+ const was = this.cache.etag;
1467
+ const now2 = await etag(this.transport, { ifNoneMatch: was });
1468
+ if (was && now2 === was) return { changed: false, etag: was };
1469
+ this.cache.clear();
1470
+ this.cache.etag = now2;
1471
+ return { changed: true, etag: now2 };
1472
+ }
1336
1473
  /**
1337
1474
  * Load whatever the queries in a batch need to have their rows translated:
1338
1475
  * the `from` types, then the types behind every relation hop and sub-select,
@@ -1393,8 +1530,8 @@ __export(queryV1_exports, {
1393
1530
  });
1394
1531
  function rejected(error) {
1395
1532
  const details = error.details;
1396
- const messages = (details?.exceptions ?? []).map((e) => e?.message).filter((m) => typeof m === "string" && !!m);
1397
- return messages.length ? new ApiError(messages.join("; "), error.status, "QUERY_REJECTED", error.details) : error;
1533
+ const rejections = (details?.exceptions ?? []).filter((e) => e && typeof e.message === "string" && e.message).map((e) => ({ type: typeof e.type === "string" ? e.type : null, message: e.message }));
1534
+ return rejections.length ? new ApiError(rejections.map((r) => r.message).join("; "), error.status, "QUERY_REJECTED", rejections) : error;
1398
1535
  }
1399
1536
  async function queries2(transport, qs, options = {}) {
1400
1537
  for (const q of qs) if (!q.page) throw new UsageError(`query of ${q.from} without a page: every query.v1 read must be bounded`);
@@ -1427,6 +1564,8 @@ __export(rest1_exports, {
1427
1564
  history: () => history,
1428
1565
  list: () => list2,
1429
1566
  momentless: () => momentless,
1567
+ operate: () => operate,
1568
+ template: () => template,
1430
1569
  toQuery: () => toQuery
1431
1570
  });
1432
1571
  var toRef = (doc) => ({ _oid: momentless(doc.idref) });
@@ -1481,6 +1620,19 @@ async function get2(transport, oid, select) {
1481
1620
  const doc = await transport.request({ method: "GET", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { sel: select?.join(",") }, responseType: "json" });
1482
1621
  return fromAssetDocument(doc);
1483
1622
  }
1623
+ async function operate(transport, oid, op) {
1624
+ const [type, id] = momentless(oid).split(":");
1625
+ if (!type || !id) throw new UsageError(`"${oid}" is not an OID token (Story:1234)`);
1626
+ const doc = await transport.request({ method: "POST", path: `/rest-1.v1/Data/${encodeURIComponent(type)}/${encodeURIComponent(id)}`, query: { op }, responseType: "json" });
1627
+ if (!doc || typeof doc.id !== "string" || !doc.id) throw new ApiError(`rest-1 ${op} on ${oid} did not answer with an asset`, 200, "BAD_RESPONSE", doc);
1628
+ const moment = doc.id.split(":")[2] ?? null;
1629
+ return { oid: momentless(doc.id), moment };
1630
+ }
1631
+ async function template(transport, type, options = {}) {
1632
+ const doc = await transport.request({ method: "GET", path: `/rest-1.v1/New/${encodeURIComponent(type)}`, query: { ctx: options.ctx ? momentless(options.ctx) : void 0 }, responseType: "json" });
1633
+ const { _oid: _ignored, ...values } = fromAssetDocument({ ...doc, id: doc.id || `${type}:0` });
1634
+ return values;
1635
+ }
1484
1636
  var history = (transport, target, params) => {
1485
1637
  const [type, id] = target.split(":");
1486
1638
  const path = id ? `/rest-1.v1/Hist/${encodeURIComponent(type)}/${encodeURIComponent(id)}` : `/rest-1.v1/Hist/${encodeURIComponent(type)}`;
@@ -1563,11 +1715,21 @@ var AgilityApiClient = class _AgilityApiClient {
1563
1715
  return row;
1564
1716
  }
1565
1717
  // ---- the endpoints behind the reads -----------------------------------------------
1566
- /** meta.v1: the instance's schema, raw. (The translated, cached view is `schema`.) */
1718
+ /**
1719
+ * meta.v1: the instance's schema, raw. (The translated, cached view is
1720
+ * `schema`; `schema.revalidate()` drops it when the schema's tag moves.)
1721
+ * `read` and `readType` are conditional: pass the tag a previous answer
1722
+ * carried and an unchanged schema answers null without a download —
1723
+ * meta.v1 sends one strong ETag for the whole schema. `etag()` reads that
1724
+ * tag alone, from the smallest resource.
1725
+ */
1567
1726
  meta = {
1568
1727
  metamodel: () => getMetamodel(this.transport),
1728
+ read: (options) => readMetamodel(this.transport, options),
1569
1729
  assetType: (token) => getAssetType(this.transport, token),
1570
- attributeDefinition: (tokenref) => getAttributeDefinition(this.transport, tokenref)
1730
+ readType: (token, options) => readAssetType(this.transport, token, options),
1731
+ attributeDefinition: (tokenref) => getAttributeDefinition(this.transport, tokenref),
1732
+ etag: (options) => etag(this.transport, options)
1571
1733
  };
1572
1734
  /**
1573
1735
  * loc-2.v1: labels for tags, in one request (meta's DisplayName values are
@@ -1578,16 +1740,39 @@ var AgilityApiClient = class _AgilityApiClient {
1578
1740
  return localize(this.transport, tags, options);
1579
1741
  }
1580
1742
  /**
1581
- * rest-1.v1, for what query.v1 cannot do: deleted assets (`deleted:
1582
- * true`), one asset by OID without a page, and history (one row per
1583
- * moment). Parameters are spelled as a query's (`select`, `filter`).
1743
+ * rest-1.v1, for what the other endpoints cannot do: deleted assets
1744
+ * (`deleted: true`), one asset by OID without a page, history (one row
1745
+ * per moment), a new asset's template (`template`: what the type's
1746
+ * initializer fills in for a context — a Timebox's next name and dates,
1747
+ * a Story's carry-over from a Request), and an operation's result
1748
+ * (`operate`: api/asset performs a Copy but never says what it made;
1749
+ * rest-1 answers the new OID). Parameters are spelled as a query's
1750
+ * (`select`, `filter`).
1584
1751
  */
1585
1752
  data = {
1586
1753
  list: (type, params) => list2(this.transport, type, params),
1587
1754
  get: (oid, select) => get2(this.transport, oid, select),
1588
- history: (target, params) => history(this.transport, target, params)
1755
+ history: (target, params) => history(this.transport, target, params),
1756
+ template: (type, options) => template(this.transport, type, options),
1757
+ operate: (oid, op) => operate(this.transport, oid, op)
1589
1758
  };
1590
- /** @deprecated since 1.0.0-alpha.3: `client.query` / `client.queries` read through query.v1. Removed in 1.0.0. */
1759
+ /** Copy an asset (the server's Copy operation) and answer the copy's OID: the one thing api/asset's `execute: "Copy"` does not say. */
1760
+ async copy(oid) {
1761
+ return (await operate(this.transport, oid, "Copy")).oid;
1762
+ }
1763
+ /**
1764
+ * What a form needs for an asset type in a scope: the attributes the
1765
+ * server requires to create one (meta), the ones the product shows there
1766
+ * (api/visibleFields, custom fields included once configured), and the
1767
+ * scope's scheme's important fields and whether they are enforced at
1768
+ * close. `visible` alone answers for several types and scopes at once.
1769
+ */
1770
+ fields = {
1771
+ describe: (type, scope) => describe2(this.transport, this.schema, type, scope),
1772
+ visible: (types, scopes) => visibleFields(this.transport, types, scopes),
1773
+ visibleByAsset: (oid) => visibleFieldsByAsset(this.transport, oid)
1774
+ };
1775
+ /** @deprecated since 1.0.0-alpha.3: use `client.query` / `client.queries` (the same endpoint, query.v1; only the name moved). Removed in 1.0.0. */
1591
1776
  queryV1 = {
1592
1777
  query: (q) => this.query(q),
1593
1778
  queries: (qs) => this.queries(qs)
@@ -1619,7 +1804,7 @@ var AgilityApiClient = class _AgilityApiClient {
1619
1804
  await this.schema.ensureFor(commands);
1620
1805
  return sendGuarded(this.transport, commands, { ...options, lookup: this.schema });
1621
1806
  },
1622
- /** @deprecated since 1.0.0-alpha.3: use `client.query` (or `client.queryPage` for a total). Removed in 1.0.0. */
1807
+ /** @deprecated since 1.0.0-alpha.3: use `client.query` (rows, through query.v1) or `client.queryPage` (rows and a total, through api/asset as this does). Removed in 1.0.0. */
1623
1808
  query: async (q) => {
1624
1809
  await this.schema.ensureFor([q]);
1625
1810
  return query(this.transport, q, { lookup: this.schema });
@@ -1679,8 +1864,19 @@ var AgilityApiClient = class _AgilityApiClient {
1679
1864
  };
1680
1865
  /** Where a browser opens things on this instance: an asset's page, an attachment's bytes, an embedded image. */
1681
1866
  urls = {
1682
- /** The asset's page: assetdetail.v1 redirects to the type's own detail page (live 2026-11-12). */
1683
- asset: (oid) => `${this.config.baseUrl.replace(/\/+$/, "")}/assetdetail.v1?oid=${momentless(oid)}`,
1867
+ /**
1868
+ * The asset's page, from an OID token or an asset Number (S-01001,
1869
+ * what a person typed and what a message shows): assetdetail.v1
1870
+ * resolves either and redirects to the type's own detail page; a signed-
1871
+ * out reader lands there after logging in. Anything else is a UsageError.
1872
+ */
1873
+ asset: (ref) => {
1874
+ const base = this.config.baseUrl.replace(/\/+$/, "");
1875
+ const text = ref.trim();
1876
+ if (isOidToken(text)) return `${base}/assetdetail.v1?oid=${momentless(text)}`;
1877
+ if (isAssetNumber(text)) return `${base}/assetdetail.v1?Number=${encodeURIComponent(text)}`;
1878
+ throw new UsageError(`"${ref}" is neither an OID token (Story:1234) nor an asset number (S-1234)`);
1879
+ },
1684
1880
  attachment: (id) => attachmentUrl(this.config.baseUrl, id),
1685
1881
  embeddedImage: (id) => embeddedImageUrl(this.config.baseUrl, id)
1686
1882
  };
@@ -1708,7 +1904,9 @@ export {
1708
1904
  attachment_exports as attachment,
1709
1905
  createFetchTransport,
1710
1906
  describe as describeBatch,
1907
+ fields_exports as fields,
1711
1908
  guard,
1909
+ importantName,
1712
1910
  isAssetNumber,
1713
1911
  isOidToken,
1714
1912
  kindOf,