@objectstack/core 17.0.0-rc.6 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1385,6 +1385,7 @@ function createMemoryJob() {
1385
1385
  }
1386
1386
 
1387
1387
  // src/fallbacks/memory-i18n.ts
1388
+ import { normalizeSupportedLocales } from "@objectstack/spec/system";
1388
1389
  function deepMerge(target, source) {
1389
1390
  const result = { ...target };
1390
1391
  for (const key of Object.keys(source)) {
@@ -1418,6 +1419,7 @@ function createMemoryI18n() {
1418
1419
  const translations = /* @__PURE__ */ new Map();
1419
1420
  const authored = /* @__PURE__ */ new Map();
1420
1421
  let defaultLocale = "en";
1422
+ let supportedLocales;
1421
1423
  function resolveKey(data, key) {
1422
1424
  const parts = key.split(".");
1423
1425
  let current = data;
@@ -1483,9 +1485,29 @@ function createMemoryI18n() {
1483
1485
  authored.set(locale, { ...data });
1484
1486
  }
1485
1487
  },
1488
+ /**
1489
+ * Report the locales this stack offers.
1490
+ *
1491
+ * [#7679] When the app declared `i18n.supportedLocales`, that declaration
1492
+ * IS the answer — in declared order, and including a declared locale no
1493
+ * bundle was ever loaded for (declared-but-unserved). Reporting the
1494
+ * declaration rather than an intersection is what gives a client the
1495
+ * signal that the locale it is being offered has nothing behind it yet;
1496
+ * quietly dropping it would leave the gap invisible on both sides. It is
1497
+ * also the only answer that does not depend on how much had loaded by the
1498
+ * time this was called.
1499
+ *
1500
+ * With nothing declared, the loaded set — the behaviour every app that
1501
+ * never opted in already has.
1502
+ */
1486
1503
  getLocales() {
1504
+ if (supportedLocales) return [...supportedLocales];
1487
1505
  return [.../* @__PURE__ */ new Set([...translations.keys(), ...authored.keys()])];
1488
1506
  },
1507
+ /** @see II18nService.setSupportedLocales — [#7679] */
1508
+ setSupportedLocales(locales) {
1509
+ supportedLocales = normalizeSupportedLocales(locales);
1510
+ },
1489
1511
  getDefaultLocale() {
1490
1512
  return defaultLocale;
1491
1513
  },
@@ -1495,14 +1517,42 @@ function createMemoryI18n() {
1495
1517
  };
1496
1518
  }
1497
1519
 
1520
+ // src/metadata-service-contract.ts
1521
+ import { pluralToSingular } from "@objectstack/spec/shared";
1522
+ var REGISTER_REFUSAL_CODE = "VALIDATION_ERROR";
1523
+ function canonicalMetadataServiceType(type) {
1524
+ return pluralToSingular(type);
1525
+ }
1526
+ function registerRefusal(message) {
1527
+ const err = new Error(message);
1528
+ err.code = REGISTER_REFUSAL_CODE;
1529
+ err.status = 400;
1530
+ return err;
1531
+ }
1532
+ function assertMetadataRegisterContract(type, name, data) {
1533
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
1534
+ const shape = data === null ? "null" : Array.isArray(data) ? "an array" : `a ${typeof data}`;
1535
+ throw registerRefusal(
1536
+ `IMetadataService.register('${type}', '${name}'): data is ${shape}, not a metadata document. register() stores plain-object documents only \u2014 accepting a value the service cannot key was measured as accept-then-drop on document-keyed stores (#7378 row 3: refuse loudly, never coerce into storability). Wrap the value in a document object whose shape the '${type}' type's schema accepts, or store it under a type that declares one.`
1537
+ );
1538
+ }
1539
+ const documentName = data.name;
1540
+ if (documentName !== void 0 && documentName !== name) {
1541
+ throw registerRefusal(
1542
+ `IMetadataService.register('${type}', '${name}'): data.name is '${String(documentName)}', which disagrees with the name argument '${name}'. A disagreement is almost always an authoring bug, and resolving it silently in either direction can file the item under a key the caller never wrote (#7378 row 1: refuse loudly, locate the mismatch). Register under one name: pass the intended key as the argument and make data.name match it, or omit data.name.`
1543
+ );
1544
+ }
1545
+ }
1546
+
1498
1547
  // src/fallbacks/memory-metadata.ts
1499
1548
  function createMemoryMetadata() {
1500
1549
  const store = /* @__PURE__ */ new Map();
1501
1550
  function getTypeMap(type) {
1502
- let map = store.get(type);
1551
+ const canonical = canonicalMetadataServiceType(type);
1552
+ let map = store.get(canonical);
1503
1553
  if (!map) {
1504
1554
  map = /* @__PURE__ */ new Map();
1505
- store.set(type, map);
1555
+ store.set(canonical, map);
1506
1556
  }
1507
1557
  return map;
1508
1558
  }
@@ -1517,6 +1567,7 @@ function createMemoryMetadata() {
1517
1567
  },
1518
1568
  _serviceName: "metadata",
1519
1569
  async register(type, name, data) {
1570
+ assertMetadataRegisterContract(type, name, data);
1520
1571
  getTypeMap(type).set(name, data);
1521
1572
  },
1522
1573
  // Mirror MetadataManager.registerInMemory (synchronous, no persistence).
@@ -1528,7 +1579,11 @@ function createMemoryMetadata() {
1528
1579
  // so `defineStack({ datasources })` entries silently never reached the
1529
1580
  // registry and were absent from GET /api/v1/datasources and
1530
1581
  // GET /api/v1/meta/datasource (ADR-0015 §18). This store is already
1531
- // in-memory only, so registerInMemory and register share an implementation.
1582
+ // in-memory only, so registerInMemory and register share a store — but
1583
+ // NOT the [#7378] refusals: the ruling names `register`, and this member
1584
+ // is a boot-time seeding primitive for source-control-owned artefacts
1585
+ // (see assertMetadataRegisterContract's header for the boundary). It does
1586
+ // share the row-2 canonical type fold, via getTypeMap.
1532
1587
  registerInMemory(type, name, data) {
1533
1588
  getTypeMap(type).set(name, data);
1534
1589
  },
@@ -2524,11 +2579,29 @@ var TestRunner = class {
2524
2579
  };
2525
2580
 
2526
2581
  // src/qa/http-adapter.ts
2582
+ import { RestApiConfigSchema, CrudEndpointsConfigSchema } from "@objectstack/spec/api";
2583
+ var dataPathCache;
2584
+ function defaultDataPath() {
2585
+ if (dataPathCache === void 0) {
2586
+ const api = RestApiConfigSchema.parse({});
2587
+ const crud = CrudEndpointsConfigSchema.parse({});
2588
+ dataPathCache = `${api.apiPath ?? `${api.basePath}/${api.version}`}${crud.dataPrefix}`;
2589
+ }
2590
+ return dataPathCache;
2591
+ }
2527
2592
  var HttpTestAdapter = class {
2528
2593
  constructor(baseUrl, authToken) {
2529
2594
  this.baseUrl = baseUrl;
2530
2595
  this.authToken = authToken;
2531
2596
  }
2597
+ /** `{baseUrl}{apiBasePath}{dataPrefix}/{object}` — the collection URL. */
2598
+ collectionUrl(objectName) {
2599
+ return `${this.baseUrl}${defaultDataPath()}/${encodeURIComponent(objectName)}`;
2600
+ }
2601
+ /** `{collection}/{id}` — the single-record URL. */
2602
+ recordUrl(objectName, id) {
2603
+ return `${this.collectionUrl(objectName)}/${encodeURIComponent(String(id))}`;
2604
+ }
2532
2605
  async execute(action, _context) {
2533
2606
  const headers = {
2534
2607
  "Content-Type": "application/json"
@@ -2560,7 +2633,7 @@ var HttpTestAdapter = class {
2560
2633
  }
2561
2634
  }
2562
2635
  async createRecord(objectName, data, headers) {
2563
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}`, {
2636
+ const response = await fetch(this.collectionUrl(objectName), {
2564
2637
  method: "POST",
2565
2638
  headers,
2566
2639
  body: JSON.stringify(data)
@@ -2568,19 +2641,19 @@ var HttpTestAdapter = class {
2568
2641
  return this.handleResponse(response);
2569
2642
  }
2570
2643
  async updateRecord(objectName, data, headers) {
2571
- const id = data.id;
2644
+ const { id, ...fields } = data;
2572
2645
  if (!id) throw new Error("Update record requires id in payload");
2573
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2574
- method: "PUT",
2646
+ const response = await fetch(this.recordUrl(objectName, id), {
2647
+ method: "PATCH",
2575
2648
  headers,
2576
- body: JSON.stringify(data)
2649
+ body: JSON.stringify(fields)
2577
2650
  });
2578
2651
  return this.handleResponse(response);
2579
2652
  }
2580
2653
  async deleteRecord(objectName, data, headers) {
2581
2654
  const id = data.id;
2582
2655
  if (!id) throw new Error("Delete record requires id in payload");
2583
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2656
+ const response = await fetch(this.recordUrl(objectName, id), {
2584
2657
  method: "DELETE",
2585
2658
  headers
2586
2659
  });
@@ -2589,14 +2662,14 @@ var HttpTestAdapter = class {
2589
2662
  async readRecord(objectName, data, headers) {
2590
2663
  const id = data.id;
2591
2664
  if (!id) throw new Error("Read record requires id in payload");
2592
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2665
+ const response = await fetch(this.recordUrl(objectName, id), {
2593
2666
  method: "GET",
2594
2667
  headers
2595
2668
  });
2596
2669
  return this.handleResponse(response);
2597
2670
  }
2598
2671
  async queryRecords(objectName, data, headers) {
2599
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/query`, {
2672
+ const response = await fetch(`${this.collectionUrl(objectName)}/query`, {
2600
2673
  method: "POST",
2601
2674
  headers,
2602
2675
  body: JSON.stringify(data)
@@ -4546,6 +4619,18 @@ function shouldDenyAnonymous(input) {
4546
4619
  return true;
4547
4620
  }
4548
4621
 
4622
+ // src/security/audience-binding-suggestion-status.ts
4623
+ var AUDIENCE_BINDING_SUGGESTION_STATUSES = {
4624
+ pending: true,
4625
+ confirmed: true,
4626
+ dismissed: true
4627
+ };
4628
+ var AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES = Object.keys(
4629
+ AUDIENCE_BINDING_SUGGESTION_STATUSES
4630
+ );
4631
+ var isAudienceBindingSuggestionStatus = (value) => Object.prototype.hasOwnProperty.call(AUDIENCE_BINDING_SUGGESTION_STATUSES, value);
4632
+ var unknownAudienceBindingSuggestionStatusMessage = (value) => `Unknown status filter '${value}' \u2014 expected one of: ${AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES.join(", ")}`;
4633
+
4549
4634
  // src/security/operation-private-keys.ts
4550
4635
  var OPERATION_PRIVATE_KEY_PREFIX = "__";
4551
4636
  function withoutOperationPrivateKeys(exec) {
@@ -4799,6 +4884,27 @@ async function bulkWrite(rows, opts) {
4799
4884
  return results;
4800
4885
  }
4801
4886
 
4887
+ // src/utils/internal-write-response.ts
4888
+ function collectInternalWriteResponseFields(schema) {
4889
+ const fields = schema?.fields;
4890
+ if (!fields || typeof fields !== "object") return [];
4891
+ const out = [];
4892
+ for (const [name, def] of Object.entries(fields)) {
4893
+ if (def && def.internal === true) out.push(name);
4894
+ }
4895
+ return out;
4896
+ }
4897
+ function omitInternalFieldsFromWriteResponse(schema, records) {
4898
+ if (!records) return;
4899
+ const internalFields = collectInternalWriteResponseFields(schema);
4900
+ if (internalFields.length === 0) return;
4901
+ const list = Array.isArray(records) ? records : [records];
4902
+ for (const row of list) {
4903
+ if (!row || typeof row !== "object") continue;
4904
+ for (const field of internalFields) delete row[field];
4905
+ }
4906
+ }
4907
+
4802
4908
  // src/utils/migration-journal.ts
4803
4909
  import { createHash as createHash2, randomUUID } from "crypto";
4804
4910
  import {
@@ -5394,6 +5500,15 @@ function filterTokenContextFrom(execCtx, now) {
5394
5500
  };
5395
5501
  }
5396
5502
 
5503
+ // src/utils/record-not-found.ts
5504
+ function recordNotFoundError(object, id) {
5505
+ const err = new Error(`Record ${id} not found in ${object}`);
5506
+ err.code = "RECORD_NOT_FOUND";
5507
+ err.status = 404;
5508
+ err.object = object;
5509
+ return err;
5510
+ }
5511
+
5397
5512
  // src/health-monitor.ts
5398
5513
  var PluginHealthMonitor = class {
5399
5514
  constructor(logger) {
@@ -6376,6 +6491,8 @@ export {
6376
6491
  ANONYMOUS_DENY_MESSAGE,
6377
6492
  ANONYMOUS_DENY_STATUS,
6378
6493
  API_KEY_PREFIX,
6494
+ AUDIENCE_BINDING_SUGGESTION_STATUSES,
6495
+ AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
6379
6496
  CORE_FALLBACK_FACTORIES,
6380
6497
  DependencyResolver,
6381
6498
  ENTRY_EXECUTION_CONTEXT_FIELDS,
@@ -6409,11 +6526,14 @@ export {
6409
6526
  assembleExecutionContext,
6410
6527
  assembleExecutionContextOrGuest,
6411
6528
  assertInitServiceRequirements,
6529
+ assertMetadataRegisterContract,
6412
6530
  bucketKeyToCalendarRange,
6413
6531
  buildPermissionsFromGrants,
6414
6532
  bulkWrite,
6415
6533
  calendarPartsInTz,
6416
6534
  calendarPartsInTzOrUtc,
6535
+ canonicalMetadataServiceType,
6536
+ collectInternalWriteResponseFields,
6417
6537
  counterSignPayload,
6418
6538
  createLogger,
6419
6539
  createMemoryCache,
@@ -6438,6 +6558,7 @@ export {
6438
6558
  getMemoryUsage,
6439
6559
  hashApiKey,
6440
6560
  hashMigrationPlan,
6561
+ isAudienceBindingSuggestionStatus,
6441
6562
  isAuthGateAllowlisted,
6442
6563
  isExpired,
6443
6564
  isGrantActive,
@@ -6445,12 +6566,14 @@ export {
6445
6566
  isNode,
6446
6567
  nextUtcCalendarDay,
6447
6568
  normalizeAuthGate,
6569
+ omitInternalFieldsFromWriteResponse,
6448
6570
  parseScopes,
6449
6571
  parseSignature,
6450
6572
  planChunks,
6451
6573
  postureVisibleRows,
6452
6574
  readAuthoredTranslationLayer,
6453
6575
  readRunJournal,
6576
+ recordNotFoundError,
6454
6577
  resolveApiKeyPrincipal,
6455
6578
  resolveAuthzContext,
6456
6579
  resolveFilterToken,
@@ -6464,6 +6587,7 @@ export {
6464
6587
  safeExit,
6465
6588
  shouldDenyAnonymous,
6466
6589
  signPayload,
6590
+ unknownAudienceBindingSuggestionStatusMessage,
6467
6591
  utcInstantMs,
6468
6592
  validateInitServiceContract,
6469
6593
  verifyPayload,