@vectros-ai/cli 0.20.0 → 0.21.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/cli.mjs CHANGED
@@ -526,8 +526,8 @@ var require_graceful_fs = __commonJS({
526
526
  fs5.createReadStream = createReadStream;
527
527
  fs5.createWriteStream = createWriteStream;
528
528
  var fs$readFile = fs5.readFile;
529
- fs5.readFile = readFile3;
530
- function readFile3(path4, options, cb) {
529
+ fs5.readFile = readFile4;
530
+ function readFile4(path4, options, cb) {
531
531
  if (typeof options === "function")
532
532
  cb = options, options = null;
533
533
  return go$readFile(path4, options, cb);
@@ -1619,7 +1619,7 @@ var require_proper_lockfile = __commonJS({
1619
1619
  // src/cli-bootstrap.ts
1620
1620
  import * as readline2 from "readline/promises";
1621
1621
 
1622
- // node_modules/@vectros-ai/sdk/dist/index.mjs
1622
+ // ../../node_modules/@vectros-ai/sdk/dist/index.mjs
1623
1623
  var __defProp2 = Object.defineProperty;
1624
1624
  var __export = (target, all) => {
1625
1625
  for (var name in all)
@@ -1653,6 +1653,7 @@ __export(api_exports, {
1653
1653
  FieldDef: () => FieldDef,
1654
1654
  FileUploadRequest: () => FileUploadRequest,
1655
1655
  ForbiddenError: () => ForbiddenError,
1656
+ GatewayTimeoutError: () => GatewayTimeoutError,
1656
1657
  IdentityLookupRequest: () => IdentityLookupRequest,
1657
1658
  IndexFailure: () => IndexFailure,
1658
1659
  InternalServerError: () => InternalServerError,
@@ -1677,9 +1678,12 @@ __export(api_exports, {
1677
1678
  SearchRequest: () => SearchRequest,
1678
1679
  SearchResult: () => SearchResult,
1679
1680
  SearchResultsEvent: () => SearchResultsEvent,
1681
+ ServiceUnavailableError: () => ServiceUnavailableError,
1680
1682
  TooManyRequestsError: () => TooManyRequestsError,
1683
+ TriggerFailureResponse: () => TriggerFailureResponse,
1681
1684
  TruncationWarningEvent: () => TruncationWarningEvent,
1682
1685
  UnauthorizedError: () => UnauthorizedError,
1686
+ UnprocessableEntityError: () => UnprocessableEntityError,
1683
1687
  UserExistsResponse: () => UserExistsResponse,
1684
1688
  UserRequest: () => UserRequest,
1685
1689
  UserResponse: () => UserResponse,
@@ -1691,7 +1695,9 @@ __export(api_exports, {
1691
1695
  inference: () => inference_exports,
1692
1696
  records: () => records_exports,
1693
1697
  schemas: () => schemas_exports,
1694
- search: () => search_exports
1698
+ scripts: () => scripts_exports,
1699
+ search: () => search_exports,
1700
+ triggers: () => triggers_exports
1695
1701
  });
1696
1702
  var toJson = (value, replacer, space) => {
1697
1703
  return JSON.stringify(value, replacer, space);
@@ -1811,6 +1817,21 @@ var ForbiddenError = class extends VectrosError {
1811
1817
  this.name = this.constructor.name;
1812
1818
  }
1813
1819
  };
1820
+ var GatewayTimeoutError = class extends VectrosError {
1821
+ constructor(body, rawResponse) {
1822
+ super({
1823
+ message: "GatewayTimeoutError",
1824
+ statusCode: 504,
1825
+ body,
1826
+ rawResponse
1827
+ });
1828
+ Object.setPrototypeOf(this, new.target.prototype);
1829
+ if (Error.captureStackTrace) {
1830
+ Error.captureStackTrace(this, this.constructor);
1831
+ }
1832
+ this.name = this.constructor.name;
1833
+ }
1834
+ };
1814
1835
  var InternalServerError = class extends VectrosError {
1815
1836
  constructor(body, rawResponse) {
1816
1837
  super({
@@ -1871,6 +1892,21 @@ var PaymentRequiredError = class extends VectrosError {
1871
1892
  this.name = this.constructor.name;
1872
1893
  }
1873
1894
  };
1895
+ var ServiceUnavailableError = class extends VectrosError {
1896
+ constructor(body, rawResponse) {
1897
+ super({
1898
+ message: "ServiceUnavailableError",
1899
+ statusCode: 503,
1900
+ body,
1901
+ rawResponse
1902
+ });
1903
+ Object.setPrototypeOf(this, new.target.prototype);
1904
+ if (Error.captureStackTrace) {
1905
+ Error.captureStackTrace(this, this.constructor);
1906
+ }
1907
+ this.name = this.constructor.name;
1908
+ }
1909
+ };
1874
1910
  var TooManyRequestsError = class extends VectrosError {
1875
1911
  constructor(body, rawResponse) {
1876
1912
  super({
@@ -1901,6 +1937,21 @@ var UnauthorizedError = class extends VectrosError {
1901
1937
  this.name = this.constructor.name;
1902
1938
  }
1903
1939
  };
1940
+ var UnprocessableEntityError = class extends VectrosError {
1941
+ constructor(body, rawResponse) {
1942
+ super({
1943
+ message: "UnprocessableEntityError",
1944
+ statusCode: 422,
1945
+ body,
1946
+ rawResponse
1947
+ });
1948
+ Object.setPrototypeOf(this, new.target.prototype);
1949
+ if (Error.captureStackTrace) {
1950
+ Error.captureStackTrace(this, this.constructor);
1951
+ }
1952
+ this.name = this.constructor.name;
1953
+ }
1954
+ };
1904
1955
  var auth_exports = {};
1905
1956
  var ErasureRequest;
1906
1957
  ((ErasureRequest2) => {
@@ -1990,6 +2041,7 @@ var LookupRecordsRequestOrder = {
1990
2041
  Desc: "desc"
1991
2042
  };
1992
2043
  var schemas_exports = {};
2044
+ var scripts_exports = {};
1993
2045
  var SearchRequest;
1994
2046
  ((SearchRequest2) => {
1995
2047
  SearchRequest2.Mode = {
@@ -2015,6 +2067,7 @@ var search_exports = {};
2015
2067
  __export(search_exports, {
2016
2068
  SearchRequest: () => SearchRequest
2017
2069
  });
2070
+ var triggers_exports = {};
2018
2071
  var AccessProfileRequest;
2019
2072
  ((AccessProfileRequest2) => {
2020
2073
  AccessProfileRequest2.Status = {
@@ -2044,7 +2097,9 @@ var BatchWriteResult;
2044
2097
  Created: "created",
2045
2098
  Updated: "updated",
2046
2099
  Conflict: "conflict",
2047
- Invalid: "invalid"
2100
+ Invalid: "invalid",
2101
+ Forbidden: "forbidden",
2102
+ NotCommitted: "not_committed"
2048
2103
  };
2049
2104
  })(BatchWriteResult || (BatchWriteResult = {}));
2050
2105
  var ChatMessage;
@@ -2348,13 +2403,23 @@ var SearchResultsEvent;
2348
2403
  SearchResults: "search_results"
2349
2404
  };
2350
2405
  })(SearchResultsEvent || (SearchResultsEvent = {}));
2406
+ var TriggerFailureResponse;
2407
+ ((TriggerFailureResponse2) => {
2408
+ TriggerFailureResponse2.Event = {
2409
+ Create: "CREATE",
2410
+ Update: "UPDATE",
2411
+ Delete: "DELETE"
2412
+ };
2413
+ })(TriggerFailureResponse || (TriggerFailureResponse = {}));
2351
2414
  var TruncationWarningEvent;
2352
2415
  ((TruncationWarningEvent2) => {
2353
2416
  TruncationWarningEvent2.Event = {
2354
2417
  TruncationWarning: "truncation_warning"
2355
2418
  };
2356
2419
  TruncationWarningEvent2.Reason = {
2357
- ContextWindowBudget: "context_window_budget"
2420
+ ContextWindowBudget: "context_window_budget",
2421
+ NoGroundableContent: "no_groundable_content",
2422
+ ContextWindowBudgetAndNoContent: "context_window_budget_and_no_content"
2358
2423
  };
2359
2424
  })(TruncationWarningEvent || (TruncationWarningEvent = {}));
2360
2425
  var UserExistsResponse;
@@ -2369,8 +2434,7 @@ var UserRequest;
2369
2434
  ((UserRequest2) => {
2370
2435
  UserRequest2.Status = {
2371
2436
  Active: "ACTIVE",
2372
- Suspended: "SUSPENDED",
2373
- Pending: "PENDING"
2437
+ Suspended: "SUSPENDED"
2374
2438
  };
2375
2439
  UserRequest2.Type = {
2376
2440
  Human: "HUMAN",
@@ -3838,6 +3902,16 @@ function mergeHeaders(...headersArray) {
3838
3902
  }
3839
3903
  return result;
3840
3904
  }
3905
+ function mergeOnlyDefinedHeaders(...headersArray) {
3906
+ const result = {};
3907
+ for (const [key, value] of headersArray.filter((headers) => headers != null).flatMap((headers) => Object.entries(headers))) {
3908
+ const insensitiveKey = key.toLowerCase();
3909
+ if (value != null) {
3910
+ result[insensitiveKey] = value;
3911
+ }
3912
+ }
3913
+ return result;
3914
+ }
3841
3915
  function normalizeClientOptions(options) {
3842
3916
  const headers = mergeHeaders(
3843
3917
  {
@@ -4108,7 +4182,7 @@ var AuthClient = class {
4108
4182
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/keys/scoped");
4109
4183
  }
4110
4184
  /**
4111
- * Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. Minting a key bound to your OWN principal needs nothing further; minting one bound to a DIFFERENT principal additionally requires the `delegate-mint` capability (`granted_capabilities`) on your credential — without it the request is refused. A root API key (`sk_`) is exempt from all three bounds.
4185
+ * Creates a scoped API key (an `ssk_*` secret) that inherits its permissions from an existing access profile in your account. The call is idempotent on the combination of tenant, context, user, and key name: re-issuing the same request returns the existing key WITHOUT re-disclosing its raw secret. The raw key is returned ONLY in this response — store it securely, as it cannot be retrieved again. Requires the `keys:c` scope. If you use a scoped credential, `keys:c` alone is not sufficient: because the minted key is durably bound to the profile you name, the profile's effective scopes may not exceed your own, and you may only mint against a profile whose `identityOverrides` values your own identity holds. Minting a key bound to your OWN principal needs nothing further; minting one bound to a DIFFERENT principal additionally requires the `delegate-mint` capability (`granted_capabilities`) on your credential — without it the request is refused. A root API key (`sk_`) is exempt from all three bounds. The profile you name must be `active` AND the user you name must not be `SUSPENDED`: either is refused with `409`, and no root exemption applies to that. Both refusals stop ISSUANCE only. Suspending the profile additionally stops credentials already bound to it, within about five minutes; suspending the user does not stop their existing keys at all. A `PENDING` user is fine.
4112
4186
  *
4113
4187
  * @param {Vectros.CreateScopedKeyRequest} request
4114
4188
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4116,6 +4190,7 @@ var AuthClient = class {
4116
4190
  * @throws {@link Vectros.BadRequestError}
4117
4191
  * @throws {@link Vectros.ForbiddenError}
4118
4192
  * @throws {@link Vectros.NotFoundError}
4193
+ * @throws {@link Vectros.ConflictError}
4119
4194
  * @throws {@link Vectros.TooManyRequestsError}
4120
4195
  *
4121
4196
  * @example
@@ -4164,6 +4239,8 @@ var AuthClient = class {
4164
4239
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
4165
4240
  case 404:
4166
4241
  throw new NotFoundError(_response.error.body, _response.rawResponse);
4242
+ case 409:
4243
+ throw new ConflictError(_response.error.body, _response.rawResponse);
4167
4244
  case 429:
4168
4245
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
4169
4246
  default:
@@ -4575,7 +4652,7 @@ var AuthClient = class {
4575
4652
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/app-contexts");
4576
4653
  }
4577
4654
  /**
4578
- * Creates a new app context. This call is idempotent by `contextId`: if an app context with the same `contextId` already exists, the existing app context is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing context was returned) tells the two apart. To overwrite an existing context's `name`/`description` instead of returning it unchanged, set `?upsert=true` (this also requires the `app-contexts:u` scope). The reserved `contextId` value `vectros-admin` cannot be created through this endpoint; it is provisioned automatically for your account. Requires the `app-contexts:c` scope.
4655
+ * Creates a new app context. This call is idempotent by `contextId`: if an app context with the same `contextId` already exists, the existing app context is returned (with status 200) instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing context was returned) tells the two apart. To overwrite an existing context's `name`/`description`/`companyName` instead of returning it unchanged, set `?upsert=true` (this also requires the `app-contexts:u` scope). The reserved `contextId` value `vectros-admin` cannot be created through this endpoint; it is provisioned automatically for your account. Requires the `app-contexts:c` scope.
4579
4656
  *
4580
4657
  * @param {Vectros.CreateAppContextRequest} request
4581
4658
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5059,7 +5136,7 @@ var AuthClient = class {
5059
5136
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/app-contexts/{contextId}");
5060
5137
  }
5061
5138
  /**
5062
- * Updates the name and/or description of an app context. This is a partial update: any field you omit (or send as null) keeps its existing value. The `contextId` is immutable and is taken from the URL path, so any `contextId` in the request body is ignored. Requires the `app-contexts:u` scope.
5139
+ * Updates the name, description, and/or companyName of an app context. This is a partial update: any field you omit (or send as null) keeps its existing value. The `contextId` is immutable and is taken from the URL path, so any `contextId` in the request body is ignored. Requires the `app-contexts:u` scope.
5063
5140
  *
5064
5141
  * @param {Vectros.UpdateAppContextRequest} request
5065
5142
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5541,7 +5618,7 @@ var AuthClient = class {
5541
5618
  );
5542
5619
  }
5543
5620
  /**
5544
- * Returns full usage detail for the requested calendar month, broken down by category (search, documents, and records) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access. **A token confined to a single app context sees only that context's usage**: totals, the environment split, and the `contexts` breakdown narrow to it, and the environment your context is not bound to is omitted (`null`), not zeroed. Only a token with cross-context reach sees your full account-wide totals. Two exceptions to the narrowing, since they have no per-context breakdown to narrow to: `reads.calls.used`/`reads.dataOut.bytes` (metered per account, not per context) read as `0` for a confined token rather than a narrowed figure — the corresponding overage-credit charge fields narrow correctly; and `credits.limit` stays your whole plan's ceiling, so `credits.remaining` may overstate the account's true remaining room.
5621
+ * Returns full usage detail for the requested calendar month, broken down by category (search, documents, records, and trigger-script execution time) with per-category credit estimates and a split between your live and test environments. Defaults to the current month when `year` and `month` are omitted. Requires the `billing:r` scope on scoped tokens; API keys always have access. **A token confined to a single app context sees only that context's usage**: totals, the environment split, and the `contexts` breakdown narrow to it, and the environment your context is not bound to is omitted (`null`), not zeroed. Only a token with cross-context reach sees your full account-wide totals. Two exceptions to the narrowing, since they have no per-context breakdown to narrow to: `reads.calls.used`/`reads.dataOut.bytes` (metered per account, not per context) read as `0` for a confined token rather than a narrowed figure — the corresponding overage-credit charge fields narrow correctly; and `credits.limit` stays your whole plan's ceiling, so `credits.remaining` may overstate the account's true remaining room. The `execution` section narrows correctly for a confined token, like the other per-context charge fields — but note its credits carry sub-credit remainders forward across your whole account for the period, so compare `execution.creditsMilli` against your account-wide figure rather than recomputing it from one context's `billableMillis`.
5545
5622
  *
5546
5623
  * @param {Vectros.GetUsageRequest} request
5547
5624
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7675,7 +7752,7 @@ var FoldersClient = class {
7675
7752
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/folders/{id}");
7676
7753
  }
7677
7754
  /**
7678
- * Permanently deletes a folder. The folder must be empty (contain no documents or sub-folders) and must not be protected. Your context's root folder is protected and cannot be deleted. Requires the `folders:d` scope.
7755
+ * Permanently deletes a folder. The folder must be empty — it must contain no documents, no records, and no sub-folders — and must not be protected. Your context's root folder is protected and cannot be deleted. Requires the `folders:d` scope.
7679
7756
  *
7680
7757
  * @param {Vectros.DeleteFolderRequest} request
7681
7758
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8084,11 +8161,12 @@ var IdentityClient = class {
8084
8161
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/entities/{namespace}/{id}");
8085
8162
  }
8086
8163
  /**
8087
- * Updates the mutable fields of an entity. Omitted fields are preserved (a null value does not clear a field), and the `payload` object is replaced in full when supplied. Providing `scopes` replaces the entity's parent edges. Requires the `entities:u:<namespace>` scope.
8164
+ * Updates the mutable fields of an entity. Omitted fields are preserved (a null value does not clear a field), and the `payload` object is replaced in full when supplied. Providing `scopes` replaces the entity's parent edges. Supplying a different `externalId` re-points the entity's identifier; it must still be unused in this namespace and app context. Requires the `entities:u:<namespace>` scope.
8088
8165
  *
8089
8166
  * @param {Vectros.UpdateEntityRequest} request
8090
8167
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8091
8168
  *
8169
+ * @throws {@link Vectros.BadRequestError}
8092
8170
  * @throws {@link Vectros.NotFoundError}
8093
8171
  * @throws {@link Vectros.TooManyRequestsError}
8094
8172
  *
@@ -8138,6 +8216,8 @@ var IdentityClient = class {
8138
8216
  }
8139
8217
  if (_response.error.reason === "status-code") {
8140
8218
  switch (_response.error.statusCode) {
8219
+ case 400:
8220
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
8141
8221
  case 404:
8142
8222
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8143
8223
  case 429:
@@ -9362,7 +9442,7 @@ var InferenceClient = class {
9362
9442
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/documents/{id}/ask");
9363
9443
  }
9364
9444
  /**
9365
- * Runs hybrid search over your indexed content, then streams a model answer grounded in the top results. The SSE stream emits a `search_results` event first (carrying the matched results and their metadata), an optional `truncation_warning` if lower-scoring results were dropped to fit the model's context window, then `content_delta` chunks, and finally a terminal `done` event. Requires the `inference:r` scope.
9445
+ * Runs hybrid search over your indexed content, then streams a model answer grounded in the top results. The SSE stream emits a `search_results` event first (carrying the matched results and their metadata), an optional `truncation_warning` if some retrieved results were dropped before the prompt was built — either because they didn't fit the model's context window or because a result had no groundable text to include — then `content_delta` chunks, and finally a terminal `done` event. Requires the `inference:r` scope.
9366
9446
  */
9367
9447
  ragInference(request, requestOptions) {
9368
9448
  return HttpResponsePromise.fromPromise(this.__ragInference(request, requestOptions));
@@ -9557,14 +9637,18 @@ var RecordsClient = class {
9557
9637
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/records/lookup/batch");
9558
9638
  }
9559
9639
  /**
9560
- * Reserved endpoint for bulk record writes. The published response shape includes a per-item partial-failure envelope and an atomicity flag. It currently returns 501 (not implemented). The documented 200 response schema is the stable shape this endpoint will use once available, published now so SDK integrations against it will not break when it ships. Requires the `records:c` scope.
9640
+ * Writes up to 50 records (or 50 with `atomicity: all_or_nothing`) in a single request. Each item in `items` has exactly the same shape, and goes through exactly the same validation, authorization and idempotency rules, as the body of a single `POST /v1/records` — including schema validation, `externalId` idempotency, unique-field enforcement, and the `records:c:<type>` scope check, which is applied per item against that item's own record type. A batch is not a way to write a type your credential could not write one at a time.
9641
+ *
9642
+ * `atomicity` selects how the batch commits. `best_effort` (the default) writes each item independently: some can succeed while others fail, and each item's own outcome is reported. `all_or_nothing` commits every item in one transaction — if any item fails, no record is created or updated at all, and every item that was itself fine reports `not_committed` while the ones that failed report why. Because the transaction is bounded by the number of underlying storage rows rather than by the number of records, a large `all_or_nothing` batch can be refused as too large to commit atomically even when it is within the item limit; the error says so, and nothing is written when it happens.
9643
+ *
9644
+ * The response is HTTP 200 whenever the batch was processed at all — including when every item failed — so always inspect `results` rather than relying on the status code. Match each result to the item you sent using its `index`. Requires the `records:c` scope.
9561
9645
  *
9562
9646
  * @param {Vectros.BatchWriteRequest} request
9563
9647
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9564
9648
  *
9649
+ * @throws {@link Vectros.BadRequestError}
9565
9650
  * @throws {@link Vectros.ForbiddenError}
9566
9651
  * @throws {@link Vectros.TooManyRequestsError}
9567
- * @throws {@link Vectros.NotImplementedError}
9568
9652
  *
9569
9653
  * @example
9570
9654
  * await client.records.batchWriteRecords()
@@ -9573,6 +9657,11 @@ var RecordsClient = class {
9573
9657
  return HttpResponsePromise.fromPromise(this.__batchWriteRecords(request, requestOptions));
9574
9658
  }
9575
9659
  async __batchWriteRecords(request = {}, requestOptions) {
9660
+ const { upsert, allowClear, ..._body } = request;
9661
+ const _queryParams = {
9662
+ upsert,
9663
+ allowClear
9664
+ };
9576
9665
  const _authRequest = await this._options.authProvider.getAuthRequest();
9577
9666
  const _headers = mergeHeaders(
9578
9667
  _authRequest.headers,
@@ -9587,9 +9676,9 @@ var RecordsClient = class {
9587
9676
  method: "POST",
9588
9677
  headers: _headers,
9589
9678
  contentType: "application/json",
9590
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
9679
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
9591
9680
  requestType: "json",
9592
- body: request,
9681
+ body: _body,
9593
9682
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
9594
9683
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
9595
9684
  abortSignal: requestOptions?.abortSignal,
@@ -9601,12 +9690,12 @@ var RecordsClient = class {
9601
9690
  }
9602
9691
  if (_response.error.reason === "status-code") {
9603
9692
  switch (_response.error.statusCode) {
9693
+ case 400:
9694
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
9604
9695
  case 403:
9605
9696
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
9606
9697
  case 429:
9607
9698
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
9608
- case 501:
9609
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
9610
9699
  default:
9611
9700
  throw new VectrosError({
9612
9701
  statusCode: _response.error.statusCode,
@@ -10509,7 +10598,7 @@ var SchemasClient = class {
10509
10598
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/schemas/{id}");
10510
10599
  }
10511
10600
  /**
10512
- * Updates a record schema. Fields you omit are preserved; `typeName` is immutable and cannot be changed. Collection fields (`fields`, `lookupFields`, `renderHints`, `capabilities`) are replaced in full when supplied. Requires the `schemas:u` scope.
10601
+ * Updates a record schema. Fields you omit are preserved; `typeName` is immutable and cannot be changed. Collection fields (`fields`, `lookupFields`, `renderHints`, `capabilities`) are replaced in full when supplied. Because `capabilities` is replaced in full, omitting `triggersEnabled` clears it — the request is refused with 409 if that would disable triggers while trigger rules still fire off this schema, so delete those rules first. Requires the `schemas:u` scope.
10513
10602
  *
10514
10603
  * @param {Vectros.UpdateSchemaRequest} request
10515
10604
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10517,6 +10606,7 @@ var SchemasClient = class {
10517
10606
  * @throws {@link Vectros.BadRequestError}
10518
10607
  * @throws {@link Vectros.ForbiddenError}
10519
10608
  * @throws {@link Vectros.NotFoundError}
10609
+ * @throws {@link Vectros.ConflictError}
10520
10610
  * @throws {@link Vectros.TooManyRequestsError}
10521
10611
  *
10522
10612
  * @example
@@ -10568,6 +10658,8 @@ var SchemasClient = class {
10568
10658
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
10569
10659
  case 404:
10570
10660
  throw new NotFoundError(_response.error.body, _response.rawResponse);
10661
+ case 409:
10662
+ throw new ConflictError(_response.error.body, _response.rawResponse);
10571
10663
  case 429:
10572
10664
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
10573
10665
  default:
@@ -10581,7 +10673,7 @@ var SchemasClient = class {
10581
10673
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/schemas/{id}");
10582
10674
  }
10583
10675
  /**
10584
- * Permanently deletes a record schema. The request is refused with 409 if records of this type still exist — delete those records first, since every record must reference a live schema. A lineage base (a schema other schemas declare `basedOn`) also cannot be deleted while any such variant still exists — delete the variant schema(s) first. Requires the `schemas:d` scope.
10676
+ * Permanently deletes a record schema. The request is refused with 409 if records of this type still exist — delete those records first, since every record must reference a live schema. A lineage base (a schema other schemas declare `basedOn`) also cannot be deleted while any such variant still exists — delete the variant schema(s) first. It is likewise refused while any trigger rule fires off this schema — delete those trigger rules first. Requires the `schemas:d` scope.
10585
10677
  *
10586
10678
  * @param {Vectros.DeleteSchemaRequest} request
10587
10679
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10703,46 +10795,70 @@ var SchemasClient = class {
10703
10795
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/schemas/{id}/versions");
10704
10796
  }
10705
10797
  };
10706
- var SearchClient = class {
10798
+ var ScriptsClient = class {
10707
10799
  constructor(options) {
10708
10800
  this._options = normalizeClientOptionsWithAuth(options);
10709
10801
  }
10710
10802
  /**
10711
- * Runs a hybrid, semantic, or text search across the content you have indexed. By default it returns both documents and records in a single unified result set; pass `contentTypes` to narrow the results (for example `["documents"]`). Each result carries a `sourceType` discriminator (`PartnerDocument` or `GenericRecord`) so you can branch on the type. Requires the `search:r` scope.
10803
+ * Runs a script you previously pushed to `POST /v1/scripts`, under your own credential, and returns what it returned. Every write the script makes is buffered and committed as ONE transaction after the script finishes: either all of them land or none do, and the response is sent only after they have committed — so everything in `result` is committed state or the script's own computation over it. On any failure the body is the standard error envelope and nothing the script staged is committed: its rows are written only when the transaction commits, and a document body or a large payload is DECIDED at that point and WRITTEN to storage only after the commit, so an aborted execution leaves nothing behind. The one residual sits after a successful commit, outside this response: if storing a body fails then, the row exists and the body does not until the platform's repair lands — so a `200` guarantees the rows, and a document's text should be read before it is relied on.
10712
10804
  *
10713
- * @param {Vectros.SearchRequest} request
10714
- * @param {SearchClient.RequestOptions} requestOptions - Request-specific configuration.
10805
+ * **Permission.** Requires the `scripts:x` scope — the permission to *execute* — separate from the `scripts:c` permission to push scripts. Bare `scripts:x` grants every script in the context; `scripts:x:<name>` grants exactly that script name, every version of it. Execution discloses no source, so `scripts:r` is not needed. Inside the script, every read and write is enforced by your credential's data scopes exactly as the equivalent REST call would be — `scripts:x` alone can read and write nothing.
10806
+ *
10807
+ * **Input.** Your script sees `input.event` (the literal `"execute"`), `input.userId` (your credential's user id, or null), and `input.params` (the `input` object you sent, verbatim). That is the same `input` grammar a trigger rule uses, so one script can serve both by branching on `input.event`. `params` is not validated — a script that needs a shape checks for it and throws; the script's `declaredInputContract` is documentation for readers and is not enforced.
10808
+ *
10809
+ * **Budget.** Seconds of useful work: the script has 15 s of wall clock (never more than 16) and a bounded number of statements and host calls; a runaway script is stopped early with `RESOURCE_LIMIT_EXCEEDED`. The transaction is bounded by the number of underlying storage rows it writes — 100 rows after merging — rather than by the number of entities: creating a folder costs 3 rows; creating a plain text document or a record costs 1 row plus one per range-indexed lookup field plus one per `reference` field. So "create a folder and file N documents" holds up to 97 plain documents, 48 with one range field or one reference, 32 with both; if a script exceeds the limit the error says so (`WRITE_BUFFER_CAP_EXCEEDED`, with `total` and `limit`) and nothing is committed. This request takes the write-path limits (burst, credit ceiling, principal quota) once, before the script runs — there is no free-read allowance here, so an account over its credit ceiling is refused even for a read-only script. Reads the script performs are metered as reads, writes as writes, and execution time beyond what those operations include is charged as script execution time (see `GET /v1/usage`).
10810
+ *
10811
+ * **Reads inside the script.** A `get` sees the script's own staged writes. A list, query or lookup never shows a row the script has deleted, shows a row it re-saved with the updated content, and does not yet show a row it created — you hold that row's id from `create()`.
10812
+ *
10813
+ * **Retries.** Send an optional `Idempotency-Key` header (1–128 characters of letters, digits, `.`, `_`, `:` or `-`) to make a retry safe: a request repeated under the same key within 24 hours receives the same response as the first attempt without executing again — when that first attempt SUCCEEDED (a duplicate still in flight is refused with `409 IDEMPOTENCY_IN_PROGRESS`; the same key on a different request is refused with `422 IDEMPOTENCY_KEY_REUSED`). A first attempt that FAILED with any other status is not remembered: the same key is free to try again, so a retry after a `400` or a `429` executes. If an execution's outcome cannot be determined — the script did not finish inside the budget while its writes may have committed — the response is `500 EXECUTION_OUTCOME_UNKNOWN` carrying `execution.id`, and a keyed retry receives that same answer: inspect what the script would have written, then use a NEW key for a new attempt. When a key is present the serialised `result` is capped at 16 KB instead of 256 KB. Without a key, a lost response cannot be distinguished from a failed one; re-read before retrying.
10814
+ *
10815
+ * @param {Vectros.ScriptExecuteRequest} request
10816
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
10715
10817
  *
10716
10818
  * @throws {@link Vectros.BadRequestError}
10819
+ * @throws {@link Vectros.PaymentRequiredError}
10717
10820
  * @throws {@link Vectros.ForbiddenError}
10821
+ * @throws {@link Vectros.NotFoundError}
10822
+ * @throws {@link Vectros.ConflictError}
10823
+ * @throws {@link Vectros.ContentTooLargeError}
10824
+ * @throws {@link Vectros.UnprocessableEntityError}
10718
10825
  * @throws {@link Vectros.TooManyRequestsError}
10826
+ * @throws {@link Vectros.InternalServerError}
10827
+ * @throws {@link Vectros.ServiceUnavailableError}
10828
+ * @throws {@link Vectros.GatewayTimeoutError}
10719
10829
  *
10720
10830
  * @example
10721
- * await client.search.content({
10722
- * query: "patient intake form diabetes"
10831
+ * await client.scripts.executeScript({
10832
+ * "Idempotency-Key": "order-8412-attempt",
10833
+ * scriptRef: {
10834
+ * name: "notify-assignee",
10835
+ * version: "3"
10836
+ * }
10723
10837
  * })
10724
10838
  */
10725
- content(request, requestOptions) {
10726
- return HttpResponsePromise.fromPromise(this.__content(request, requestOptions));
10839
+ executeScript(request, requestOptions) {
10840
+ return HttpResponsePromise.fromPromise(this.__executeScript(request, requestOptions));
10727
10841
  }
10728
- async __content(request, requestOptions) {
10842
+ async __executeScript(request, requestOptions) {
10843
+ const { "Idempotency-Key": idempotencyKey, ..._body } = request;
10729
10844
  const _authRequest = await this._options.authProvider.getAuthRequest();
10730
10845
  const _headers = mergeHeaders(
10731
10846
  _authRequest.headers,
10732
10847
  this._options?.headers,
10848
+ mergeOnlyDefinedHeaders({ "Idempotency-Key": idempotencyKey }),
10733
10849
  requestOptions?.headers
10734
10850
  );
10735
10851
  const _response = await fetcher({
10736
10852
  url: url_exports.join(
10737
10853
  await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
10738
- "v1/search"
10854
+ "v1/scripts/execute"
10739
10855
  ),
10740
10856
  method: "POST",
10741
10857
  headers: _headers,
10742
10858
  contentType: "application/json",
10743
10859
  queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
10744
10860
  requestType: "json",
10745
- body: request,
10861
+ body: _body,
10746
10862
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
10747
10863
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
10748
10864
  abortSignal: requestOptions?.abortSignal,
@@ -10756,10 +10872,26 @@ var SearchClient = class {
10756
10872
  switch (_response.error.statusCode) {
10757
10873
  case 400:
10758
10874
  throw new BadRequestError(_response.error.body, _response.rawResponse);
10875
+ case 402:
10876
+ throw new PaymentRequiredError(_response.error.body, _response.rawResponse);
10759
10877
  case 403:
10760
10878
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
10879
+ case 404:
10880
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
10881
+ case 409:
10882
+ throw new ConflictError(_response.error.body, _response.rawResponse);
10883
+ case 413:
10884
+ throw new ContentTooLargeError(_response.error.body, _response.rawResponse);
10885
+ case 422:
10886
+ throw new UnprocessableEntityError(_response.error.body, _response.rawResponse);
10761
10887
  case 429:
10762
10888
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
10889
+ case 500:
10890
+ throw new InternalServerError(_response.error.body, _response.rawResponse);
10891
+ case 503:
10892
+ throw new ServiceUnavailableError(_response.error.body, _response.rawResponse);
10893
+ case 504:
10894
+ throw new GatewayTimeoutError(_response.error.body, _response.rawResponse);
10763
10895
  default:
10764
10896
  throw new VectrosError({
10765
10897
  statusCode: _response.error.statusCode,
@@ -10768,71 +10900,834 @@ var SearchClient = class {
10768
10900
  });
10769
10901
  }
10770
10902
  }
10771
- return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/search");
10772
- }
10773
- };
10774
- var VectrosClient = class {
10775
- constructor(options) {
10776
- this._options = normalizeClientOptionsWithAuth(options);
10777
- }
10778
- get auth() {
10779
- return this._auth ?? (this._auth = new AuthClient(this._options));
10780
- }
10781
- get documents() {
10782
- return this._documents ?? (this._documents = new DocumentsClient(this._options));
10783
- }
10784
- get identity() {
10785
- return this._identity ?? (this._identity = new IdentityClient(this._options));
10786
- }
10787
- get compliance() {
10788
- return this._compliance ?? (this._compliance = new ComplianceClient(this._options));
10789
- }
10790
- get folders() {
10791
- return this._folders ?? (this._folders = new FoldersClient(this._options));
10792
- }
10793
- get inference() {
10794
- return this._inference ?? (this._inference = new InferenceClient(this._options));
10903
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/scripts/execute");
10795
10904
  }
10796
- get records() {
10797
- return this._records ?? (this._records = new RecordsClient(this._options));
10798
- }
10799
- get schemas() {
10800
- return this._schemas ?? (this._schemas = new SchemasClient(this._options));
10905
+ /**
10906
+ * Returns a paginated list of your scripts. Pass `name` to list every version of one script name (oldest first); omit it for a flat list across every name in your account (newest first). Results are returned as a `{data, nextCursor}` envelope — pass `nextCursor` as `startFrom` to fetch the next page. Requires the `scripts:r` scope.
10907
+ *
10908
+ * @param {Vectros.ListScriptsRequest} request
10909
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
10910
+ *
10911
+ * @example
10912
+ * await client.scripts.listScripts({
10913
+ * name: "notify-assignee",
10914
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
10915
+ * })
10916
+ */
10917
+ listScripts(request = {}, requestOptions) {
10918
+ return HttpResponsePromise.fromPromise(this.__listScripts(request, requestOptions));
10801
10919
  }
10802
- get search() {
10803
- return this._search ?? (this._search = new SearchClient(this._options));
10920
+ async __listScripts(request = {}, requestOptions) {
10921
+ const { name, startFrom, limit } = request;
10922
+ const _queryParams = {
10923
+ name,
10924
+ startFrom,
10925
+ limit
10926
+ };
10927
+ const _authRequest = await this._options.authProvider.getAuthRequest();
10928
+ const _headers = mergeHeaders(
10929
+ _authRequest.headers,
10930
+ this._options?.headers,
10931
+ requestOptions?.headers
10932
+ );
10933
+ const _response = await fetcher({
10934
+ url: url_exports.join(
10935
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
10936
+ "v1/scripts"
10937
+ ),
10938
+ method: "GET",
10939
+ headers: _headers,
10940
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
10941
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
10942
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
10943
+ abortSignal: requestOptions?.abortSignal,
10944
+ fetchFn: this._options?.fetch,
10945
+ logging: this._options.logging
10946
+ });
10947
+ if (_response.ok) {
10948
+ return { data: _response.body, rawResponse: _response.rawResponse };
10949
+ }
10950
+ if (_response.error.reason === "status-code") {
10951
+ throw new VectrosError({
10952
+ statusCode: _response.error.statusCode,
10953
+ body: _response.error.body,
10954
+ rawResponse: _response.rawResponse
10955
+ });
10956
+ }
10957
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/scripts");
10804
10958
  }
10805
10959
  /**
10806
- * Make a passthrough request using the SDK's configured auth, retry, logging, etc.
10807
- * This is useful for making requests to endpoints not yet supported in the SDK.
10808
- * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL.
10960
+ * Stores a new version of a script object. Every POST creates a genuinely new version — this is NOT idempotent-by-name: pushing `name` again always creates a new row with `scriptVersion` auto-incremented (the first version pushed for a name is 1). `source` is stored as-is: nothing parses or validates it at push time. A version runs when a trigger rule references it (`POST /v1/triggers`) or when `POST /v1/scripts/execute` names it. Requires the `scripts:c` scope.
10809
10961
  *
10810
- * @param {Request | string | URL} input - The URL, path, or Request object.
10811
- * @param {RequestInit} init - Standard fetch RequestInit options.
10812
- * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal).
10813
- * @returns {Promise<Response>} A standard Response object.
10962
+ * @param {Vectros.ScriptRequest} request
10963
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
10964
+ *
10965
+ * @throws {@link Vectros.BadRequestError}
10966
+ * @throws {@link Vectros.ForbiddenError}
10967
+ * @throws {@link Vectros.ConflictError}
10968
+ * @throws {@link Vectros.TooManyRequestsError}
10969
+ *
10970
+ * @example
10971
+ * await client.scripts.createScript({
10972
+ * name: "notify-assignee",
10973
+ * source: "export default function(input) { return { ok: true }; }"
10974
+ * })
10814
10975
  */
10815
- async fetch(input, init, requestOptions) {
10816
- return makePassthroughRequest(
10817
- input,
10818
- init,
10819
- {
10820
- baseUrl: this._options.baseUrl ?? this._options.environment,
10821
- headers: this._options.headers,
10822
- timeoutInSeconds: this._options.timeoutInSeconds,
10823
- maxRetries: this._options.maxRetries,
10824
- fetch: this._options.fetch,
10825
- logging: this._options.logging,
10826
- getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers
10827
- },
10828
- requestOptions
10976
+ createScript(request, requestOptions) {
10977
+ return HttpResponsePromise.fromPromise(this.__createScript(request, requestOptions));
10978
+ }
10979
+ async __createScript(request, requestOptions) {
10980
+ const _authRequest = await this._options.authProvider.getAuthRequest();
10981
+ const _headers = mergeHeaders(
10982
+ _authRequest.headers,
10983
+ this._options?.headers,
10984
+ requestOptions?.headers
10829
10985
  );
10986
+ const _response = await fetcher({
10987
+ url: url_exports.join(
10988
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
10989
+ "v1/scripts"
10990
+ ),
10991
+ method: "POST",
10992
+ headers: _headers,
10993
+ contentType: "application/json",
10994
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
10995
+ requestType: "json",
10996
+ body: request,
10997
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
10998
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
10999
+ abortSignal: requestOptions?.abortSignal,
11000
+ fetchFn: this._options?.fetch,
11001
+ logging: this._options.logging
11002
+ });
11003
+ if (_response.ok) {
11004
+ return { data: _response.body, rawResponse: _response.rawResponse };
11005
+ }
11006
+ if (_response.error.reason === "status-code") {
11007
+ switch (_response.error.statusCode) {
11008
+ case 400:
11009
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
11010
+ case 403:
11011
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
11012
+ case 409:
11013
+ throw new ConflictError(_response.error.body, _response.rawResponse);
11014
+ case 429:
11015
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11016
+ default:
11017
+ throw new VectrosError({
11018
+ statusCode: _response.error.statusCode,
11019
+ body: _response.error.body,
11020
+ rawResponse: _response.rawResponse
11021
+ });
11022
+ }
11023
+ }
11024
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/scripts");
10830
11025
  }
10831
- };
10832
- var logging;
10833
- ((logging2) => {
10834
- logging2.LogLevel = LogLevel;
10835
- logging2.ConsoleLogger = ConsoleLogger;
11026
+ /**
11027
+ * Retrieves a single script version by its ID. Requires the `scripts:r` scope.
11028
+ *
11029
+ * @param {Vectros.GetScriptRequest} request
11030
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
11031
+ *
11032
+ * @throws {@link Vectros.NotFoundError}
11033
+ *
11034
+ * @example
11035
+ * await client.scripts.getScript({
11036
+ * id: "id"
11037
+ * })
11038
+ */
11039
+ getScript(request, requestOptions) {
11040
+ return HttpResponsePromise.fromPromise(this.__getScript(request, requestOptions));
11041
+ }
11042
+ async __getScript(request, requestOptions) {
11043
+ const { id } = request;
11044
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11045
+ const _headers = mergeHeaders(
11046
+ _authRequest.headers,
11047
+ this._options?.headers,
11048
+ requestOptions?.headers
11049
+ );
11050
+ const _response = await fetcher({
11051
+ url: url_exports.join(
11052
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11053
+ `v1/scripts/${url_exports.encodePathParam(id)}`
11054
+ ),
11055
+ method: "GET",
11056
+ headers: _headers,
11057
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11058
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11059
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11060
+ abortSignal: requestOptions?.abortSignal,
11061
+ fetchFn: this._options?.fetch,
11062
+ logging: this._options.logging
11063
+ });
11064
+ if (_response.ok) {
11065
+ return { data: _response.body, rawResponse: _response.rawResponse };
11066
+ }
11067
+ if (_response.error.reason === "status-code") {
11068
+ switch (_response.error.statusCode) {
11069
+ case 404:
11070
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
11071
+ default:
11072
+ throw new VectrosError({
11073
+ statusCode: _response.error.statusCode,
11074
+ body: _response.error.body,
11075
+ rawResponse: _response.rawResponse
11076
+ });
11077
+ }
11078
+ }
11079
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/scripts/{id}");
11080
+ }
11081
+ /**
11082
+ * Scripts are immutable per version; this endpoint always returns a 400 regardless of scope or credential — push a new version via `POST /v1/scripts` instead.
11083
+ *
11084
+ * @param {Vectros.UpdateScriptRequest} request
11085
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
11086
+ *
11087
+ * @throws {@link Vectros.BadRequestError}
11088
+ * @throws {@link Vectros.TooManyRequestsError}
11089
+ *
11090
+ * @example
11091
+ * await client.scripts.updateScript({
11092
+ * id: "id",
11093
+ * body: {
11094
+ * name: "notify-assignee",
11095
+ * source: "export default function(input) { return { ok: true }; }"
11096
+ * }
11097
+ * })
11098
+ */
11099
+ updateScript(request, requestOptions) {
11100
+ return HttpResponsePromise.fromPromise(this.__updateScript(request, requestOptions));
11101
+ }
11102
+ async __updateScript(request, requestOptions) {
11103
+ const { id, body: _body } = request;
11104
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11105
+ const _headers = mergeHeaders(
11106
+ _authRequest.headers,
11107
+ this._options?.headers,
11108
+ requestOptions?.headers
11109
+ );
11110
+ const _response = await fetcher({
11111
+ url: url_exports.join(
11112
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11113
+ `v1/scripts/${url_exports.encodePathParam(id)}`
11114
+ ),
11115
+ method: "PUT",
11116
+ headers: _headers,
11117
+ contentType: "application/json",
11118
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11119
+ requestType: "json",
11120
+ body: _body,
11121
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11122
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11123
+ abortSignal: requestOptions?.abortSignal,
11124
+ fetchFn: this._options?.fetch,
11125
+ logging: this._options.logging
11126
+ });
11127
+ if (_response.ok) {
11128
+ return { data: void 0, rawResponse: _response.rawResponse };
11129
+ }
11130
+ if (_response.error.reason === "status-code") {
11131
+ switch (_response.error.statusCode) {
11132
+ case 400:
11133
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
11134
+ case 429:
11135
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11136
+ default:
11137
+ throw new VectrosError({
11138
+ statusCode: _response.error.statusCode,
11139
+ body: _response.error.body,
11140
+ rawResponse: _response.rawResponse
11141
+ });
11142
+ }
11143
+ }
11144
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/scripts/{id}");
11145
+ }
11146
+ /**
11147
+ * Permanently deletes one script version by ID. Deleting one version does not affect any other version of the same name. Requires the `scripts:d` scope.
11148
+ *
11149
+ * @param {Vectros.DeleteScriptRequest} request
11150
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
11151
+ *
11152
+ * @throws {@link Vectros.NotFoundError}
11153
+ * @throws {@link Vectros.TooManyRequestsError}
11154
+ *
11155
+ * @example
11156
+ * await client.scripts.deleteScript({
11157
+ * id: "id"
11158
+ * })
11159
+ */
11160
+ deleteScript(request, requestOptions) {
11161
+ return HttpResponsePromise.fromPromise(this.__deleteScript(request, requestOptions));
11162
+ }
11163
+ async __deleteScript(request, requestOptions) {
11164
+ const { id } = request;
11165
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11166
+ const _headers = mergeHeaders(
11167
+ _authRequest.headers,
11168
+ this._options?.headers,
11169
+ requestOptions?.headers
11170
+ );
11171
+ const _response = await fetcher({
11172
+ url: url_exports.join(
11173
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11174
+ `v1/scripts/${url_exports.encodePathParam(id)}`
11175
+ ),
11176
+ method: "DELETE",
11177
+ headers: _headers,
11178
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11179
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11180
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11181
+ abortSignal: requestOptions?.abortSignal,
11182
+ fetchFn: this._options?.fetch,
11183
+ logging: this._options.logging
11184
+ });
11185
+ if (_response.ok) {
11186
+ return { data: void 0, rawResponse: _response.rawResponse };
11187
+ }
11188
+ if (_response.error.reason === "status-code") {
11189
+ switch (_response.error.statusCode) {
11190
+ case 404:
11191
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
11192
+ case 429:
11193
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11194
+ default:
11195
+ throw new VectrosError({
11196
+ statusCode: _response.error.statusCode,
11197
+ body: _response.error.body,
11198
+ rawResponse: _response.rawResponse
11199
+ });
11200
+ }
11201
+ }
11202
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/scripts/{id}");
11203
+ }
11204
+ };
11205
+ var SearchClient = class {
11206
+ constructor(options) {
11207
+ this._options = normalizeClientOptionsWithAuth(options);
11208
+ }
11209
+ /**
11210
+ * Runs a hybrid, semantic, or text search across the content you have indexed. By default it returns both documents and records in a single unified result set; pass `contentTypes` to narrow the results (for example `["documents"]`). Each result carries a `sourceType` discriminator (`PartnerDocument` or `GenericRecord`) so you can branch on the type. Requires the `search:r` scope.
11211
+ *
11212
+ * @param {Vectros.SearchRequest} request
11213
+ * @param {SearchClient.RequestOptions} requestOptions - Request-specific configuration.
11214
+ *
11215
+ * @throws {@link Vectros.BadRequestError}
11216
+ * @throws {@link Vectros.ForbiddenError}
11217
+ * @throws {@link Vectros.TooManyRequestsError}
11218
+ *
11219
+ * @example
11220
+ * await client.search.content({
11221
+ * query: "patient intake form diabetes"
11222
+ * })
11223
+ */
11224
+ content(request, requestOptions) {
11225
+ return HttpResponsePromise.fromPromise(this.__content(request, requestOptions));
11226
+ }
11227
+ async __content(request, requestOptions) {
11228
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11229
+ const _headers = mergeHeaders(
11230
+ _authRequest.headers,
11231
+ this._options?.headers,
11232
+ requestOptions?.headers
11233
+ );
11234
+ const _response = await fetcher({
11235
+ url: url_exports.join(
11236
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11237
+ "v1/search"
11238
+ ),
11239
+ method: "POST",
11240
+ headers: _headers,
11241
+ contentType: "application/json",
11242
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11243
+ requestType: "json",
11244
+ body: request,
11245
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11246
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11247
+ abortSignal: requestOptions?.abortSignal,
11248
+ fetchFn: this._options?.fetch,
11249
+ logging: this._options.logging
11250
+ });
11251
+ if (_response.ok) {
11252
+ return { data: _response.body, rawResponse: _response.rawResponse };
11253
+ }
11254
+ if (_response.error.reason === "status-code") {
11255
+ switch (_response.error.statusCode) {
11256
+ case 400:
11257
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
11258
+ case 403:
11259
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
11260
+ case 429:
11261
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11262
+ default:
11263
+ throw new VectrosError({
11264
+ statusCode: _response.error.statusCode,
11265
+ body: _response.error.body,
11266
+ rawResponse: _response.rawResponse
11267
+ });
11268
+ }
11269
+ }
11270
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/search");
11271
+ }
11272
+ };
11273
+ var TriggersClient = class {
11274
+ constructor(options) {
11275
+ this._options = normalizeClientOptionsWithAuth(options);
11276
+ }
11277
+ /**
11278
+ * Returns a page of trigger executions that failed — what went wrong, whether the platform will retry it, and which of your records and rules were involved. Use this to find automation that has stopped running.
11279
+ *
11280
+ * A record appears once per FIRING, not once per attempt: if the platform retries a retryable failure, the same record's `attempts` count rises rather than a new record appearing. **If a retry later succeeds, the record is removed** — so this endpoint describes what is currently broken, not a permanent history. Records also expire 30 days after the firing first failed.
11281
+ *
11282
+ * `detail` is safe to surface to your own users: it carries only your own content (a script's own thrown message) or public API vocabulary, never platform internals. For `INTERNAL_ERROR` it is absent and a `correlationId` is returned instead — quote that to support.
11283
+ *
11284
+ * Scoped to your account and app context, derived from your token — never from input. Requires the `triggers:r` scope.
11285
+ *
11286
+ * @param {Vectros.ListTriggerFailuresRequest} request
11287
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11288
+ *
11289
+ * @example
11290
+ * await client.triggers.listTriggerFailures({
11291
+ * ruleId: "3b1c9f2e7a5d8410f6c2e9a1b7d4038f5e6a91c2d7f0b3a4586c9e2f1d0a7b3c",
11292
+ * category: "SCRIPT_ERROR",
11293
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
11294
+ * })
11295
+ */
11296
+ listTriggerFailures(request = {}, requestOptions) {
11297
+ return HttpResponsePromise.fromPromise(this.__listTriggerFailures(request, requestOptions));
11298
+ }
11299
+ async __listTriggerFailures(request = {}, requestOptions) {
11300
+ const { ruleId, category, retryable, from: from_, to, startFrom, limit } = request;
11301
+ const _queryParams = {
11302
+ ruleId,
11303
+ category,
11304
+ retryable,
11305
+ from: from_,
11306
+ to,
11307
+ startFrom,
11308
+ limit
11309
+ };
11310
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11311
+ const _headers = mergeHeaders(
11312
+ _authRequest.headers,
11313
+ this._options?.headers,
11314
+ requestOptions?.headers
11315
+ );
11316
+ const _response = await fetcher({
11317
+ url: url_exports.join(
11318
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11319
+ "v1/trigger-failures"
11320
+ ),
11321
+ method: "GET",
11322
+ headers: _headers,
11323
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
11324
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11325
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11326
+ abortSignal: requestOptions?.abortSignal,
11327
+ fetchFn: this._options?.fetch,
11328
+ logging: this._options.logging
11329
+ });
11330
+ if (_response.ok) {
11331
+ return { data: _response.body, rawResponse: _response.rawResponse };
11332
+ }
11333
+ if (_response.error.reason === "status-code") {
11334
+ throw new VectrosError({
11335
+ statusCode: _response.error.statusCode,
11336
+ body: _response.error.body,
11337
+ rawResponse: _response.rawResponse
11338
+ });
11339
+ }
11340
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/trigger-failures");
11341
+ }
11342
+ /**
11343
+ * Returns a paginated list of your trigger rules, newest first. Results are returned as a `{data, nextCursor}` envelope — pass `nextCursor` as `startFrom` to fetch the next page. Requires the `triggers:r` scope.
11344
+ *
11345
+ * @param {Vectros.ListTriggersRequest} request
11346
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11347
+ *
11348
+ * @example
11349
+ * await client.triggers.listTriggers({
11350
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
11351
+ * })
11352
+ */
11353
+ listTriggers(request = {}, requestOptions) {
11354
+ return HttpResponsePromise.fromPromise(this.__listTriggers(request, requestOptions));
11355
+ }
11356
+ async __listTriggers(request = {}, requestOptions) {
11357
+ const { startFrom, limit } = request;
11358
+ const _queryParams = {
11359
+ startFrom,
11360
+ limit
11361
+ };
11362
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11363
+ const _headers = mergeHeaders(
11364
+ _authRequest.headers,
11365
+ this._options?.headers,
11366
+ requestOptions?.headers
11367
+ );
11368
+ const _response = await fetcher({
11369
+ url: url_exports.join(
11370
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11371
+ "v1/triggers"
11372
+ ),
11373
+ method: "GET",
11374
+ headers: _headers,
11375
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
11376
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11377
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11378
+ abortSignal: requestOptions?.abortSignal,
11379
+ fetchFn: this._options?.fetch,
11380
+ logging: this._options.logging
11381
+ });
11382
+ if (_response.ok) {
11383
+ return { data: _response.body, rawResponse: _response.rawResponse };
11384
+ }
11385
+ if (_response.error.reason === "status-code") {
11386
+ throw new VectrosError({
11387
+ statusCode: _response.error.statusCode,
11388
+ body: _response.error.body,
11389
+ rawResponse: _response.rawResponse
11390
+ });
11391
+ }
11392
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/triggers");
11393
+ }
11394
+ /**
11395
+ * Declares a new trigger rule, or (with `?upsert=true`) reconciles an existing one of the same name to the submitted shape. Once declared, the rule fires: a record write on the named schema dispatches the referenced script, which runs asynchronously under this rule's own grant. Because that grant is executed, a scoped credential may not declare a trigger whose grant exceeds its own — such a request is rejected with a `403`, and a `roleIds` entry that does not resolve in this context with a `400`. Requires the `triggers:c` scope (also `triggers:u` when `?upsert=true` matches an existing row).
11396
+ *
11397
+ * @param {Vectros.CreateTriggerRequest} request
11398
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11399
+ *
11400
+ * @throws {@link Vectros.BadRequestError}
11401
+ * @throws {@link Vectros.ForbiddenError}
11402
+ * @throws {@link Vectros.TooManyRequestsError}
11403
+ *
11404
+ * @example
11405
+ * await client.triggers.createTrigger({
11406
+ * body: {
11407
+ * name: "on-intake-create",
11408
+ * firingSource: {
11409
+ * schemaId: "sch_9f8e7d6c",
11410
+ * event: "CREATE"
11411
+ * },
11412
+ * scriptRef: {
11413
+ * name: "notify-assignee",
11414
+ * version: "3"
11415
+ * }
11416
+ * }
11417
+ * })
11418
+ */
11419
+ createTrigger(request, requestOptions) {
11420
+ return HttpResponsePromise.fromPromise(this.__createTrigger(request, requestOptions));
11421
+ }
11422
+ async __createTrigger(request, requestOptions) {
11423
+ const { upsert, body: _body } = request;
11424
+ const _queryParams = {
11425
+ upsert
11426
+ };
11427
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11428
+ const _headers = mergeHeaders(
11429
+ _authRequest.headers,
11430
+ this._options?.headers,
11431
+ requestOptions?.headers
11432
+ );
11433
+ const _response = await fetcher({
11434
+ url: url_exports.join(
11435
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11436
+ "v1/triggers"
11437
+ ),
11438
+ method: "POST",
11439
+ headers: _headers,
11440
+ contentType: "application/json",
11441
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
11442
+ requestType: "json",
11443
+ body: _body,
11444
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11445
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11446
+ abortSignal: requestOptions?.abortSignal,
11447
+ fetchFn: this._options?.fetch,
11448
+ logging: this._options.logging
11449
+ });
11450
+ if (_response.ok) {
11451
+ return { data: _response.body, rawResponse: _response.rawResponse };
11452
+ }
11453
+ if (_response.error.reason === "status-code") {
11454
+ switch (_response.error.statusCode) {
11455
+ case 400:
11456
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
11457
+ case 403:
11458
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
11459
+ case 429:
11460
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11461
+ default:
11462
+ throw new VectrosError({
11463
+ statusCode: _response.error.statusCode,
11464
+ body: _response.error.body,
11465
+ rawResponse: _response.rawResponse
11466
+ });
11467
+ }
11468
+ }
11469
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/triggers");
11470
+ }
11471
+ /**
11472
+ * Retrieves a single trigger rule by its ID. Requires the `triggers:r` scope.
11473
+ *
11474
+ * @param {Vectros.GetTriggerRequest} request
11475
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11476
+ *
11477
+ * @throws {@link Vectros.NotFoundError}
11478
+ *
11479
+ * @example
11480
+ * await client.triggers.getTrigger({
11481
+ * id: "id"
11482
+ * })
11483
+ */
11484
+ getTrigger(request, requestOptions) {
11485
+ return HttpResponsePromise.fromPromise(this.__getTrigger(request, requestOptions));
11486
+ }
11487
+ async __getTrigger(request, requestOptions) {
11488
+ const { id } = request;
11489
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11490
+ const _headers = mergeHeaders(
11491
+ _authRequest.headers,
11492
+ this._options?.headers,
11493
+ requestOptions?.headers
11494
+ );
11495
+ const _response = await fetcher({
11496
+ url: url_exports.join(
11497
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11498
+ `v1/triggers/${url_exports.encodePathParam(id)}`
11499
+ ),
11500
+ method: "GET",
11501
+ headers: _headers,
11502
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11503
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11504
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11505
+ abortSignal: requestOptions?.abortSignal,
11506
+ fetchFn: this._options?.fetch,
11507
+ logging: this._options.logging
11508
+ });
11509
+ if (_response.ok) {
11510
+ return { data: _response.body, rawResponse: _response.rawResponse };
11511
+ }
11512
+ if (_response.error.reason === "status-code") {
11513
+ switch (_response.error.statusCode) {
11514
+ case 404:
11515
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
11516
+ default:
11517
+ throw new VectrosError({
11518
+ statusCode: _response.error.statusCode,
11519
+ body: _response.error.body,
11520
+ rawResponse: _response.rawResponse
11521
+ });
11522
+ }
11523
+ }
11524
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/triggers/{id}");
11525
+ }
11526
+ /**
11527
+ * Updates an existing trigger rule in place. The rule's resulting grant must be within your own granted scope — including a grant you did not change, because a trigger's other fields (its `scriptRef`, its `principalId`) determine what that grant executes. `name`, `principalId` and `provisionedBy` are immutable once set. Requires the `triggers:u` scope.
11528
+ *
11529
+ * @param {Vectros.UpdateTriggerRequest} request
11530
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11531
+ *
11532
+ * @throws {@link Vectros.BadRequestError}
11533
+ * @throws {@link Vectros.ForbiddenError}
11534
+ * @throws {@link Vectros.NotFoundError}
11535
+ * @throws {@link Vectros.TooManyRequestsError}
11536
+ *
11537
+ * @example
11538
+ * await client.triggers.updateTrigger({
11539
+ * id: "id",
11540
+ * body: {
11541
+ * name: "on-intake-create",
11542
+ * firingSource: {
11543
+ * schemaId: "sch_9f8e7d6c",
11544
+ * event: "CREATE"
11545
+ * },
11546
+ * scriptRef: {
11547
+ * name: "notify-assignee",
11548
+ * version: "3"
11549
+ * }
11550
+ * }
11551
+ * })
11552
+ */
11553
+ updateTrigger(request, requestOptions) {
11554
+ return HttpResponsePromise.fromPromise(this.__updateTrigger(request, requestOptions));
11555
+ }
11556
+ async __updateTrigger(request, requestOptions) {
11557
+ const { id, body: _body } = request;
11558
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11559
+ const _headers = mergeHeaders(
11560
+ _authRequest.headers,
11561
+ this._options?.headers,
11562
+ requestOptions?.headers
11563
+ );
11564
+ const _response = await fetcher({
11565
+ url: url_exports.join(
11566
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11567
+ `v1/triggers/${url_exports.encodePathParam(id)}`
11568
+ ),
11569
+ method: "PUT",
11570
+ headers: _headers,
11571
+ contentType: "application/json",
11572
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11573
+ requestType: "json",
11574
+ body: _body,
11575
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11576
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11577
+ abortSignal: requestOptions?.abortSignal,
11578
+ fetchFn: this._options?.fetch,
11579
+ logging: this._options.logging
11580
+ });
11581
+ if (_response.ok) {
11582
+ return { data: _response.body, rawResponse: _response.rawResponse };
11583
+ }
11584
+ if (_response.error.reason === "status-code") {
11585
+ switch (_response.error.statusCode) {
11586
+ case 400:
11587
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
11588
+ case 403:
11589
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
11590
+ case 404:
11591
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
11592
+ case 429:
11593
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11594
+ default:
11595
+ throw new VectrosError({
11596
+ statusCode: _response.error.statusCode,
11597
+ body: _response.error.body,
11598
+ rawResponse: _response.rawResponse
11599
+ });
11600
+ }
11601
+ }
11602
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/triggers/{id}");
11603
+ }
11604
+ /**
11605
+ * Permanently deletes one trigger rule by ID. Requires the `triggers:d` scope.
11606
+ *
11607
+ * @param {Vectros.DeleteTriggerRequest} request
11608
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11609
+ *
11610
+ * @throws {@link Vectros.NotFoundError}
11611
+ * @throws {@link Vectros.TooManyRequestsError}
11612
+ *
11613
+ * @example
11614
+ * await client.triggers.deleteTrigger({
11615
+ * id: "id"
11616
+ * })
11617
+ */
11618
+ deleteTrigger(request, requestOptions) {
11619
+ return HttpResponsePromise.fromPromise(this.__deleteTrigger(request, requestOptions));
11620
+ }
11621
+ async __deleteTrigger(request, requestOptions) {
11622
+ const { id } = request;
11623
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11624
+ const _headers = mergeHeaders(
11625
+ _authRequest.headers,
11626
+ this._options?.headers,
11627
+ requestOptions?.headers
11628
+ );
11629
+ const _response = await fetcher({
11630
+ url: url_exports.join(
11631
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11632
+ `v1/triggers/${url_exports.encodePathParam(id)}`
11633
+ ),
11634
+ method: "DELETE",
11635
+ headers: _headers,
11636
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11637
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11638
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11639
+ abortSignal: requestOptions?.abortSignal,
11640
+ fetchFn: this._options?.fetch,
11641
+ logging: this._options.logging
11642
+ });
11643
+ if (_response.ok) {
11644
+ return { data: void 0, rawResponse: _response.rawResponse };
11645
+ }
11646
+ if (_response.error.reason === "status-code") {
11647
+ switch (_response.error.statusCode) {
11648
+ case 404:
11649
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
11650
+ case 429:
11651
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11652
+ default:
11653
+ throw new VectrosError({
11654
+ statusCode: _response.error.statusCode,
11655
+ body: _response.error.body,
11656
+ rawResponse: _response.rawResponse
11657
+ });
11658
+ }
11659
+ }
11660
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/triggers/{id}");
11661
+ }
11662
+ };
11663
+ var VectrosClient = class {
11664
+ constructor(options) {
11665
+ this._options = normalizeClientOptionsWithAuth(options);
11666
+ }
11667
+ get auth() {
11668
+ return this._auth ?? (this._auth = new AuthClient(this._options));
11669
+ }
11670
+ get documents() {
11671
+ return this._documents ?? (this._documents = new DocumentsClient(this._options));
11672
+ }
11673
+ get identity() {
11674
+ return this._identity ?? (this._identity = new IdentityClient(this._options));
11675
+ }
11676
+ get compliance() {
11677
+ return this._compliance ?? (this._compliance = new ComplianceClient(this._options));
11678
+ }
11679
+ get folders() {
11680
+ return this._folders ?? (this._folders = new FoldersClient(this._options));
11681
+ }
11682
+ get inference() {
11683
+ return this._inference ?? (this._inference = new InferenceClient(this._options));
11684
+ }
11685
+ get records() {
11686
+ return this._records ?? (this._records = new RecordsClient(this._options));
11687
+ }
11688
+ get schemas() {
11689
+ return this._schemas ?? (this._schemas = new SchemasClient(this._options));
11690
+ }
11691
+ get scripts() {
11692
+ return this._scripts ?? (this._scripts = new ScriptsClient(this._options));
11693
+ }
11694
+ get search() {
11695
+ return this._search ?? (this._search = new SearchClient(this._options));
11696
+ }
11697
+ get triggers() {
11698
+ return this._triggers ?? (this._triggers = new TriggersClient(this._options));
11699
+ }
11700
+ /**
11701
+ * Make a passthrough request using the SDK's configured auth, retry, logging, etc.
11702
+ * This is useful for making requests to endpoints not yet supported in the SDK.
11703
+ * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL.
11704
+ *
11705
+ * @param {Request | string | URL} input - The URL, path, or Request object.
11706
+ * @param {RequestInit} init - Standard fetch RequestInit options.
11707
+ * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal).
11708
+ * @returns {Promise<Response>} A standard Response object.
11709
+ */
11710
+ async fetch(input, init, requestOptions) {
11711
+ return makePassthroughRequest(
11712
+ input,
11713
+ init,
11714
+ {
11715
+ baseUrl: this._options.baseUrl ?? this._options.environment,
11716
+ headers: this._options.headers,
11717
+ timeoutInSeconds: this._options.timeoutInSeconds,
11718
+ maxRetries: this._options.maxRetries,
11719
+ fetch: this._options.fetch,
11720
+ logging: this._options.logging,
11721
+ getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers
11722
+ },
11723
+ requestOptions
11724
+ );
11725
+ }
11726
+ };
11727
+ var logging;
11728
+ ((logging2) => {
11729
+ logging2.LogLevel = LogLevel;
11730
+ logging2.ConsoleLogger = ConsoleLogger;
10836
11731
  })(logging || (logging = {}));
10837
11732
 
10838
11733
  // ../blueprints/dist/index.mjs
@@ -10884,6 +11779,20 @@ var BlueprintFieldDefSchema = z.object({
10884
11779
  // the `s` reveal scope for this record type (SchemaRequest.FieldDef.sensitive).
10885
11780
  // Format passthrough — the loader forwards it to createSchema. Default false.
10886
11781
  sensitive: z.boolean().optional(),
11782
+ // Keep this field on the RECORD ROW when the payload is stored out of line (a large record, or a
11783
+ // LARGE_PAYLOAD storage profile), so it appears in list/lookup projections without
11784
+ // `includePayload`.
11785
+ //
11786
+ // Load-bearing for triggers: a trigger rule's `fields` may name a field only if it is `inline`,
11787
+ // `filterable`, or a lookup field — and never `sensitive`. So this is the flag that makes an
11788
+ // otherwise-ordinary field projectable into a triggered script's `input.record`. Mutually
11789
+ // exclusive with `sensitive` (the platform refuses the combination). The platform also refuses,
11790
+ // with a 409, removing whatever keeps a field projectable while a live trigger rule still
11791
+ // projects it — dropping `inline` OR `filterable` OR the lookup that carried it, not just this
11792
+ // flag.
11793
+ //
11794
+ // Format passthrough — the loader forwards it to createSchema. Default false.
11795
+ inline: z.boolean().optional(),
10887
11796
  // Reference-field surface — a typed foreign-key link to another record. The
10888
11797
  // platform (SchemaRequest.FieldDef) requires BOTH targetTypeName AND
10889
11798
  // targetSurface on a reference field; the blueprint format uses the SAME names
@@ -10906,6 +11815,13 @@ var BlueprintFieldDefSchema = z.object({
10906
11815
  targetSurface: z.string().min(1).optional(),
10907
11816
  cardinality: z.enum(["one", "many"]).optional()
10908
11817
  }).strict().superRefine((field, ctx) => {
11818
+ if (field.inline === true && field.sensitive === true) {
11819
+ ctx.addIssue({
11820
+ code: z.ZodIssueCode.custom,
11821
+ path: ["inline"],
11822
+ message: "a field cannot be both 'inline' and 'sensitive' \u2014 a sensitive field is blind-indexed and masked in responses, so it can never be projected into a trigger script; drop one of the two"
11823
+ });
11824
+ }
10909
11825
  const isReference = field.fieldType === "reference";
10910
11826
  const hasRefKeys = field.targetTypeName !== void 0 || field.targetField !== void 0 || field.targetSurface !== void 0 || field.cardinality !== void 0;
10911
11827
  if (isReference && field.targetTypeName === void 0) {
@@ -10999,7 +11915,8 @@ var BlueprintLookupFieldSchema = z.union([
10999
11915
  })
11000
11916
  ]);
11001
11917
  var BlueprintCapabilitiesSchema = z.object({
11002
- auditHistory: z.boolean().optional()
11918
+ auditHistory: z.boolean().optional(),
11919
+ triggersEnabled: z.boolean().optional()
11003
11920
  }).strict();
11004
11921
  var BlueprintSchemaSchema = z.object({
11005
11922
  typeName: z.string().min(1),
@@ -11107,6 +12024,30 @@ var GrantedCapabilitiesSchema = z.array(z.string().min(1)).optional().superRefin
11107
12024
  seen.add(name);
11108
12025
  });
11109
12026
  });
12027
+ var MAX_ASSIGNABLE_ROLES = 20;
12028
+ var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
12029
+ var AssignableRolesSchema = z.array(
12030
+ z.string().regex(ROLE_ID_RE, {
12031
+ message: "an assignableRoles entry must be a roleId: 3-31 chars, a lowercase letter first, then lowercase letters/digits/dashes (e.g. 'case-handler')"
12032
+ })
12033
+ ).min(1, {
12034
+ message: "assignableRoles must either be omitted (no roleId restriction) or name at least one roleId \u2014 an empty list states no rule"
12035
+ }).max(MAX_ASSIGNABLE_ROLES, {
12036
+ message: `assignableRoles accepts at most ${MAX_ASSIGNABLE_ROLES} entries`
12037
+ }).optional().superRefine((roleIds, ctx) => {
12038
+ if (!roleIds) return;
12039
+ const seen = /* @__PURE__ */ new Set();
12040
+ roleIds.forEach((rid, i) => {
12041
+ if (seen.has(rid)) {
12042
+ ctx.addIssue({
12043
+ code: z.ZodIssueCode.custom,
12044
+ path: [i],
12045
+ message: `assignableRoles names '${rid}' more than once \u2014 list it once`
12046
+ });
12047
+ }
12048
+ seen.add(rid);
12049
+ });
12050
+ });
11110
12051
  var BlueprintAccessProfileSchema = z.object({
11111
12052
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
11112
12053
  // what enforces the data-plane-only security boundary.
@@ -11176,7 +12117,14 @@ var BlueprintAccessProfileSchema = z.object({
11176
12117
  // blast-radius question from granting the same capability to a role that
11177
12118
  // a person is bound to. The gate should weigh the two separately rather
11178
12119
  // than reuse one policy for both.
11179
- capabilities: GrantedCapabilitiesSchema
12120
+ capabilities: GrantedCapabilitiesSchema,
12121
+ // Optional roleId allow-list on this profile's own inline clause — see
12122
+ // {@link AssignableRolesSchema}. Allowed here for the same symmetry reason `capabilities` is:
12123
+ // the platform models an access profile's scopes as a list of the SAME ScopeClause a role clause
12124
+ // uses, and the loader already builds this profile as one such clause. Only meaningful when the
12125
+ // clause grants role-composing authority in the first place (`profiles:c`-shaped); on a
12126
+ // clause that grants none it is inert rather than an error, exactly as on the platform.
12127
+ assignableRoles: AssignableRolesSchema
11180
12128
  }).strict().superRefine((ap, ctx) => {
11181
12129
  const hasInline = ap.allowedActions !== void 0;
11182
12130
  const hasRoleIds = ap.roleIds !== void 0;
@@ -11203,13 +12151,21 @@ var BlueprintAccessProfileSchema = z.object({
11203
12151
  message: "accessProfile.capabilities is not valid alongside 'roleIds' \u2014 a role-composed profile has no inline clause of its own; author capabilities on the referenced role's clause instead"
11204
12152
  });
11205
12153
  }
12154
+ if (ap.assignableRoles !== void 0) {
12155
+ ctx.addIssue({
12156
+ code: z.ZodIssueCode.custom,
12157
+ path: ["assignableRoles"],
12158
+ message: "accessProfile.assignableRoles is not valid alongside 'roleIds' \u2014 a role-composed profile has no inline clause of its own; author assignableRoles on the referenced role's clause instead"
12159
+ });
12160
+ }
11206
12161
  }
11207
12162
  });
11208
12163
  var BlueprintRoleClauseSchema = z.object({
11209
12164
  allowedActions: z.array(z.string().min(1)).min(1),
11210
12165
  dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
11211
12166
  dataScopeRef: z.string().min(1).optional(),
11212
- capabilities: GrantedCapabilitiesSchema
12167
+ capabilities: GrantedCapabilitiesSchema,
12168
+ assignableRoles: AssignableRolesSchema
11213
12169
  }).strict().superRefine((c, ctx) => {
11214
12170
  if (c.dataScope !== void 0 && c.dataScopeRef !== void 0) {
11215
12171
  ctx.addIssue({
@@ -11225,6 +12181,115 @@ var BlueprintFragmentsSchema = z.record(
11225
12181
  var BlueprintRolesSchema = z.record(z.array(BlueprintRoleClauseSchema).min(1));
11226
12182
  var BlueprintRoleAssumableSchema = z.record(z.array(z.string().min(1)).min(1));
11227
12183
  var BlueprintRoleAssumableMapSchema = z.record(BlueprintRoleAssumableSchema);
12184
+ var BlueprintTriggerFiringSourceSchema = z.object({
12185
+ schemaName: z.string().min(1),
12186
+ event: z.enum(["CREATE", "UPDATE", "DELETE"])
12187
+ }).strict();
12188
+ var SCRIPT_VERSION_INTEGER_RE = /^[1-9][0-9]*$/;
12189
+ var BlueprintTriggerScriptRefSchema = z.object({
12190
+ name: z.string().min(1),
12191
+ version: z.string().min(1)
12192
+ }).strict().superRefine((ref, ctx) => {
12193
+ if (ref.version !== "latest" && !SCRIPT_VERSION_INTEGER_RE.test(ref.version)) {
12194
+ ctx.addIssue({
12195
+ code: z.ZodIssueCode.custom,
12196
+ path: ["version"],
12197
+ message: `scriptRef.version '${ref.version}' is invalid \u2014 must be a positive decimal integer (as a string, e.g. '3') or the literal 'latest'`
12198
+ });
12199
+ }
12200
+ });
12201
+ var MANIFEST_VERB_RE = /^[a-z]+(?:\.[a-z]+)+$/;
12202
+ var BlueprintTriggerManifestSchema = z.array(
12203
+ z.string().min(1).regex(MANIFEST_VERB_RE, {
12204
+ message: "a manifest entry must be '<noun>.<verb>' (e.g. 'records.update')"
12205
+ })
12206
+ ).optional();
12207
+ var BlueprintTriggerEntrySchema = z.object({
12208
+ firingSource: BlueprintTriggerFiringSourceSchema,
12209
+ scriptRef: BlueprintTriggerScriptRefSchema,
12210
+ allowedActions: z.array(z.string().min(1)).min(1).optional(),
12211
+ roleIds: z.array(z.string().min(1)).min(1).optional(),
12212
+ dataScope: z.record(z.array(z.union([z.string().min(1), z.null()]))).optional(),
12213
+ capabilities: GrantedCapabilitiesSchema,
12214
+ manifest: BlueprintTriggerManifestSchema,
12215
+ /**
12216
+ * REQUIRED. The record fields this rule projects into its script's `input.record` — and, on an
12217
+ * UPDATE firing, `input.previous` (the same fields as they were before the write). On a DELETE
12218
+ * firing `input.record` carries these fields from the deleted row, which is the only way a
12219
+ * script can see anything of it.
12220
+ *
12221
+ * **An empty list is the "I project nothing" declaration**, not an omission: the script still
12222
+ * receives `input.recordId`. It is required rather than defaulted precisely so that projecting
12223
+ * nothing is something an author STATES, not something they get by forgetting the field.
12224
+ *
12225
+ * Every entry must name a field the schema keeps INLINE — declared `inline: true`, or
12226
+ * `filterable`, or used as a lookup field (a leg of one, or its `sortBy` target) — and never one
12227
+ * marked `sensitive`.
12228
+ * `lintTriggerFields` checks that against the schema this blueprint declares; the platform
12229
+ * checks it again, and refuses a schema update that would un-inline a field a live rule still
12230
+ * projects.
12231
+ *
12232
+ * A field the record does not carry arrives as `undefined`, never null. `record` and `previous`
12233
+ * together are capped at 224 KB serialised — a firing whose projected input exceeds that is not
12234
+ * run, and is recorded as an `INPUT_TOO_LARGE` failure naming the size.
12235
+ */
12236
+ fields: z.array(z.string().min(1))
12237
+ }).strict().superRefine((entry, ctx) => {
12238
+ const hasInline = entry.allowedActions !== void 0;
12239
+ const hasRoleIds = entry.roleIds !== void 0;
12240
+ if (hasInline === hasRoleIds) {
12241
+ ctx.addIssue({
12242
+ code: z.ZodIssueCode.custom,
12243
+ path: ["roleIds"],
12244
+ message: hasInline ? "a trigger entry carries both 'allowedActions' and 'roleIds' \u2014 provide exactly one: an inline clause (allowedActions, optionally dataScope/capabilities) OR a role composition (roleIds), never both" : "a trigger entry carries neither 'allowedActions' nor 'roleIds' \u2014 provide exactly one: an inline clause (allowedActions, optionally dataScope/capabilities) OR a role composition (roleIds)"
12245
+ });
12246
+ return;
12247
+ }
12248
+ if (hasRoleIds) {
12249
+ if (entry.dataScope !== void 0) {
12250
+ ctx.addIssue({
12251
+ code: z.ZodIssueCode.custom,
12252
+ path: ["dataScope"],
12253
+ message: "a trigger entry's dataScope is not valid alongside 'roleIds' \u2014 a role-composed entry has no inline clause of its own; author dataScope on the referenced role's clause instead"
12254
+ });
12255
+ }
12256
+ if (entry.capabilities !== void 0) {
12257
+ ctx.addIssue({
12258
+ code: z.ZodIssueCode.custom,
12259
+ path: ["capabilities"],
12260
+ message: "a trigger entry's capabilities is not valid alongside 'roleIds' \u2014 a role-composed entry has no inline clause of its own; author capabilities on the referenced role's clause instead"
12261
+ });
12262
+ }
12263
+ }
12264
+ });
12265
+ var BlueprintTriggersSchema = z.record(BlueprintTriggerEntrySchema);
12266
+ var SCRIPT_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
12267
+ var SCRIPT_SOURCE_MAX_BYTES = 3e5;
12268
+ var SCRIPT_INPUT_CONTRACT_MAX_BYTES = 5e4;
12269
+ var utf8Bytes = (s) => new TextEncoder().encode(s).length;
12270
+ var BlueprintScriptEntrySchema = z.object({
12271
+ /**
12272
+ * The script's source text, shipped verbatim.
12273
+ *
12274
+ * NOTHING PARSES THIS — not this package, and not the platform, which stores it as-is. A syntax
12275
+ * error is therefore invisible until the version first RUNS, where it surfaces as a
12276
+ * `SCRIPT_ERROR` from the trigger rule that dispatched it. Validation here is limited to what
12277
+ * can be checked without a JS parser: non-blank, and within the platform's byte cap.
12278
+ */
12279
+ source: z.string().refine((s) => s.trim().length > 0, { message: "'source' must not be blank" }).refine((s) => utf8Bytes(s) <= SCRIPT_SOURCE_MAX_BYTES, {
12280
+ message: `'source' exceeds the platform's ${SCRIPT_SOURCE_MAX_BYTES.toLocaleString("en-US")}-byte cap (measured in UTF-8 bytes, not characters)`
12281
+ }),
12282
+ /**
12283
+ * Optional free-text description of the input shape this script expects.
12284
+ *
12285
+ * DESCRIPTIVE ONLY — neither this package nor the platform validates a firing's input against
12286
+ * it. It documents the contract for a human reading the blueprint; it does not enforce one.
12287
+ */
12288
+ declaredInputContract: z.string().min(1).refine((s) => utf8Bytes(s) <= SCRIPT_INPUT_CONTRACT_MAX_BYTES, {
12289
+ message: `'declaredInputContract' exceeds the platform's ${SCRIPT_INPUT_CONTRACT_MAX_BYTES.toLocaleString("en-US")}-byte cap (measured in UTF-8 bytes, not characters)`
12290
+ }).optional()
12291
+ }).strict();
12292
+ var BlueprintScriptsSchema = z.record(BlueprintScriptEntrySchema);
11228
12293
  var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
11229
12294
  var FORBIDDEN_IDENTITY_NAMESPACES = /* @__PURE__ */ new Set(["record", "document", "entity", "user"]);
11230
12295
  var IdentityKindSchema = z.string().min(1).superRefine((val, ctx) => {
@@ -11259,6 +12324,24 @@ var BlueprintServicePrincipalSchema = z.object({
11259
12324
  displayName: z.string().min(1)
11260
12325
  }).strict();
11261
12326
  var ISSUER_ID_RE = CONTEXT_ID_RE;
12327
+ var MAX_CAPTURED_CLAIMS = 20;
12328
+ var MAX_CAPTURED_CLAIM_NAME_LENGTH = 64;
12329
+ function assertNoDuplicateClaimNames(field) {
12330
+ return (claims, ctx) => {
12331
+ if (!claims) return;
12332
+ const seen = /* @__PURE__ */ new Set();
12333
+ claims.forEach((name, i) => {
12334
+ if (seen.has(name)) {
12335
+ ctx.addIssue({
12336
+ code: z.ZodIssueCode.custom,
12337
+ path: [i],
12338
+ message: `${field} repeats '${name}' \u2014 each claim name may appear only once`
12339
+ });
12340
+ }
12341
+ seen.add(name);
12342
+ });
12343
+ };
12344
+ }
11262
12345
  var BlueprintIssuerSchema = z.object({
11263
12346
  /** Short slug identifying this issuer within your account. Immutable once registered. */
11264
12347
  issuerId: z.string().regex(ISSUER_ID_RE, {
@@ -11313,7 +12396,36 @@ var BlueprintIssuerSchema = z.object({
11313
12396
  message: "roleId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner-member')"
11314
12397
  })
11315
12398
  }).strict()
11316
- ).optional()
12399
+ ).optional(),
12400
+ /**
12401
+ * Opt-in ADDITIONAL identity-claim capture: OIDC claim names — beyond `emailClaim`, which keeps
12402
+ * its own dedicated field — read from this issuer's tokens on every successful token exchange
12403
+ * and stored as your tenant's golden IdP-asserted identity copy for the signed-in user.
12404
+ *
12405
+ * Deliberately NOT a fixed set: name whatever claims your IdP actually asserts (standard ones
12406
+ * like `name`/`phone_number`, or your provider's own custom claims). Each name is read from the
12407
+ * verified token first, falling back to `userinfoUri` (when configured) only for names still
12408
+ * missing after that — so a claim your IdP puts only on the userinfo response still resolves,
12409
+ * provided you configured that endpoint.
12410
+ *
12411
+ * Omit entirely to capture nothing beyond email (the default, and the behaviour of every
12412
+ * registration authored before this field existed). Capped at 20 entries, each a non-blank name
12413
+ * of at most 64 characters; a duplicate is REJECTED rather than silently deduplicated, because a
12414
+ * repeated name is a sign the declaration does not mean what its author thinks.
12415
+ *
12416
+ * Pairs with the top-level `identityProjectionClaims`, which decides which of these captured
12417
+ * names are then projected onto access profiles in this context — capture and projection are
12418
+ * separate opt-ins, and capturing a claim does not by itself expose it on a profile.
12419
+ */
12420
+ capturedClaims: z.array(
12421
+ z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
12422
+ message: `a capturedClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
12423
+ }).refine((name) => name.trim().length > 0, {
12424
+ message: "a capturedClaims entry must be a non-blank OIDC claim name"
12425
+ })
12426
+ ).max(MAX_CAPTURED_CLAIMS, {
12427
+ message: `capturedClaims accepts at most ${MAX_CAPTURED_CLAIMS} entries`
12428
+ }).optional().superRefine(assertNoDuplicateClaimNames("capturedClaims"))
11317
12429
  }).strict();
11318
12430
  var BlueprintNamespaceSchema = z.object({
11319
12431
  /** The namespace to register — same grammar as a `scope:<namespace>` key. */
@@ -11518,21 +12630,35 @@ var BlueprintSeedRecordSchema = z.discriminatedUnion("surface", [
11518
12630
  RecordSeedSchema,
11519
12631
  DocumentSeedSchema
11520
12632
  ]);
11521
- var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
12633
+ var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member|input)\.[^}]*?\s*\}\}/;
12634
+ var IDENTITY_DERIVED_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
12635
+ var INPUT_TOKEN_RE = /\$\{\{\s*input\.[^}]*?\s*\}\}/;
11522
12636
  function lintSelfTokenPlacement(value, ctx) {
11523
- const walk = (node, path4, inRoleDataScope) => {
12637
+ const walk = (node, path4, zone) => {
11524
12638
  if (typeof node === "string") {
11525
- if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
12639
+ if (zone === "none" && SELF_TOKEN_RE.test(node)) {
12640
+ ctx.addIssue({
12641
+ code: z.ZodIssueCode.custom,
12642
+ path: path4,
12643
+ message: "'${{ self.* }}' / '${{ member.* }}' / '${{ input.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value, a roleAssumable[] value, a fragments[] value, or a triggers[].dataScope value ('${{ input.* }}' specifically is valid ONLY in triggers[].dataScope)"
12644
+ });
12645
+ } else if (zone === "trigger" && IDENTITY_DERIVED_TOKEN_RE.test(node)) {
11526
12646
  ctx.addIssue({
11527
12647
  code: z.ZodIssueCode.custom,
11528
12648
  path: path4,
11529
- message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value, a roleAssumable[] value, or a fragments[] value"
12649
+ message: "'${{ self.* }}' / '${{ under.self.* }}' / '${{ member.* }}' resolve against the TRIGGER PRINCIPAL's identity, not yours \u2014 a trigger's grant is resolved at fire time under its own principal. The platform refuses them in a trigger grant for that reason: the reach they describe is not bounded by your own. Use '${{ input.* }}' to confine the grant to the record that fired the rule, or name literal values."
12650
+ });
12651
+ } else if (zone === "self-family" && INPUT_TOKEN_RE.test(node)) {
12652
+ ctx.addIssue({
12653
+ code: z.ZodIssueCode.custom,
12654
+ path: path4,
12655
+ message: "'${{ input.* }}' resolves against the record that fired a trigger \u2014 it is only valid inside a triggers[].dataScope value. It is inert (never resolved, matches nothing) in roles[].dataScope, roleAssumable[], and fragments[] values; use '${{ self.* }}' / '${{ under.self.* }}' / '${{ member.* }}' there instead."
11530
12656
  });
11531
12657
  }
11532
12658
  return;
11533
12659
  }
11534
12660
  if (Array.isArray(node)) {
11535
- node.forEach((v, i) => walk(v, [...path4, i], inRoleDataScope));
12661
+ node.forEach((v, i) => walk(v, [...path4, i], zone));
11536
12662
  return;
11537
12663
  }
11538
12664
  if (node && typeof node === "object") {
@@ -11540,12 +12666,13 @@ function lintSelfTokenPlacement(value, ctx) {
11540
12666
  const inRoleDataScopeClause = path4.length === 3 && path4[0] === "roles" && k === "dataScope";
11541
12667
  const inRoleAssumable = path4.length === 0 && k === "roleAssumable";
11542
12668
  const inFragments = path4.length === 0 && k === "fragments";
11543
- const entering = inRoleDataScope || inRoleDataScopeClause || inRoleAssumable || inFragments;
11544
- walk(v, [...path4, k], entering);
12669
+ const inTriggerDataScope = path4.length === 2 && path4[0] === "triggers" && k === "dataScope";
12670
+ const nextZone = zone === "trigger" || inTriggerDataScope ? "trigger" : zone === "self-family" || inRoleDataScopeClause || inRoleAssumable || inFragments ? "self-family" : "none";
12671
+ walk(v, [...path4, k], nextZone);
11545
12672
  }
11546
12673
  }
11547
12674
  };
11548
- walk(value, [], false);
12675
+ walk(value, [], "none");
11549
12676
  }
11550
12677
  var IDENTITY_REF_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
11551
12678
  var IDENTITY_REF_LOOSE_RE = /\$\{\{\s*identities\.([^}]*?)\s*\}\}/g;
@@ -11699,6 +12826,44 @@ function lintDataScopeKey(key, path4, ctx) {
11699
12826
  });
11700
12827
  }
11701
12828
  }
12829
+ var RECORD_RESERVED_FILTERABLE_FIELD_IDS = /* @__PURE__ */ new Set([
12830
+ "tenantId",
12831
+ "owner_id",
12832
+ "folderId",
12833
+ "rootFolderId",
12834
+ "recordType"
12835
+ ]);
12836
+ var DOCUMENT_RESERVED_FILTERABLE_FIELD_IDS = /* @__PURE__ */ new Set([
12837
+ "tenantId",
12838
+ "model_type",
12839
+ "owner_id",
12840
+ "title",
12841
+ "folderId",
12842
+ "rootFolderId",
12843
+ "recordType"
12844
+ ]);
12845
+ function lintReservedFilterableFieldIds(value, ctx) {
12846
+ const bp = value;
12847
+ bp.schemas?.forEach((schema, si) => {
12848
+ const surfaces = schema.allowedSurfaces ?? ["record"];
12849
+ schema.fields?.forEach((field, fi) => {
12850
+ if (!field?.filterable || !field.fieldId) return;
12851
+ const collidesWith = /* @__PURE__ */ new Set();
12852
+ if (surfaces.includes("record") && RECORD_RESERVED_FILTERABLE_FIELD_IDS.has(field.fieldId)) {
12853
+ for (const k of RECORD_RESERVED_FILTERABLE_FIELD_IDS) collidesWith.add(k);
12854
+ }
12855
+ if (surfaces.includes("document") && DOCUMENT_RESERVED_FILTERABLE_FIELD_IDS.has(field.fieldId)) {
12856
+ for (const k of DOCUMENT_RESERVED_FILTERABLE_FIELD_IDS) collidesWith.add(k);
12857
+ }
12858
+ if (collidesWith.size === 0) return;
12859
+ ctx.addIssue({
12860
+ code: z.ZodIssueCode.custom,
12861
+ path: ["schemas", si, "fields", fi, "filterable"],
12862
+ message: `schema '${schema.typeName ?? si}': field '${field.fieldId}' cannot be declared filterable \u2014 '${field.fieldId}' is a platform-owned search-index metadata key (${[...collidesWith].sort().join(", ")}) for a surface this schema binds to (${surfaces.join(", ")}), and a filterable payload field of the same name would shadow it in the index. Choose a different fieldId, or leave this field non-filterable.`
12863
+ });
12864
+ });
12865
+ });
12866
+ }
11702
12867
  function lintDataScopeKeys(value, ctx) {
11703
12868
  const bp = value;
11704
12869
  for (const key of Object.keys(bp.accessProfile?.dataScope ?? {})) {
@@ -11716,6 +12881,11 @@ function lintDataScopeKeys(value, ctx) {
11716
12881
  lintDataScopeKey(key, ["fragments", name, key], ctx);
11717
12882
  }
11718
12883
  }
12884
+ for (const [name, trigger] of Object.entries(bp.triggers ?? {})) {
12885
+ for (const key of Object.keys(trigger.dataScope ?? {})) {
12886
+ lintDataScopeKey(key, ["triggers", name, "dataScope", key], ctx);
12887
+ }
12888
+ }
11719
12889
  }
11720
12890
  function lintExpectedScopeDims(value, ctx) {
11721
12891
  const bp = value;
@@ -11811,6 +12981,150 @@ function lintAccessProfileRoleIds(value, ctx) {
11811
12981
  seen.add(roleId);
11812
12982
  });
11813
12983
  }
12984
+ function lintTriggerFields(value, ctx) {
12985
+ const bp = value;
12986
+ for (const [triggerName, entry] of Object.entries(bp.triggers ?? {})) {
12987
+ const declared = entry?.fields;
12988
+ if (!declared?.length) continue;
12989
+ const schema = bp.schemas?.find((sc) => sc.typeName === entry.firingSource?.schemaName);
12990
+ if (!schema) continue;
12991
+ const lookupNames = /* @__PURE__ */ new Set();
12992
+ for (const lf of schema.lookupFields ?? []) {
12993
+ if (typeof lf === "string") {
12994
+ lookupNames.add(lf);
12995
+ continue;
12996
+ }
12997
+ if (lf?.fieldName) lookupNames.add(lf.fieldName);
12998
+ for (const n of lf?.fieldNames ?? []) lookupNames.add(n);
12999
+ if (lf?.sortBy && lf.sortBy !== "createdAt" && lf.sortBy !== "lastUpdated") {
13000
+ lookupNames.add(lf.sortBy);
13001
+ }
13002
+ }
13003
+ const seenFields = /* @__PURE__ */ new Set();
13004
+ declared.forEach((fieldId, i) => {
13005
+ const def = schema.fields?.find((f) => f.fieldId === fieldId);
13006
+ const path4 = ["triggers", triggerName, "fields", i];
13007
+ if (seenFields.has(fieldId)) {
13008
+ ctx.addIssue({
13009
+ code: z.ZodIssueCode.custom,
13010
+ path: path4,
13011
+ message: `triggers['${triggerName}'].fields names '${fieldId}' more than once \u2014 list it once`
13012
+ });
13013
+ return;
13014
+ }
13015
+ seenFields.add(fieldId);
13016
+ if (def?.sensitive === true) {
13017
+ ctx.addIssue({
13018
+ code: z.ZodIssueCode.custom,
13019
+ path: path4,
13020
+ message: `triggers['${triggerName}'].fields names '${fieldId}', which is marked 'sensitive'. A sensitive field is blind-indexed and masked, and can never be projected into a trigger script \u2014 drop it from 'fields', or make the field non-sensitive`
13021
+ });
13022
+ return;
13023
+ }
13024
+ if (lookupNames.has(fieldId)) return;
13025
+ if (!def) {
13026
+ ctx.addIssue({
13027
+ code: z.ZodIssueCode.custom,
13028
+ path: path4,
13029
+ message: `triggers['${triggerName}'].fields names '${fieldId}', which schema '${schema.typeName}' neither declares nor uses as a lookup`
13030
+ });
13031
+ return;
13032
+ }
13033
+ if (def.inline !== true && def.filterable !== true) {
13034
+ ctx.addIssue({
13035
+ code: z.ZodIssueCode.custom,
13036
+ path: path4,
13037
+ message: `triggers['${triggerName}'].fields names '${fieldId}', which schema '${schema.typeName}' does not keep inline. A rule may project only a field that is 'inline: true', 'filterable: true', or used as a lookup field \u2014 mark it 'inline: true' on the schema, or drop it from 'fields'`
13038
+ });
13039
+ }
13040
+ });
13041
+ }
13042
+ }
13043
+ function lintScripts(value, ctx) {
13044
+ const bp = value;
13045
+ const scripts = bp.scripts;
13046
+ if (!scripts) return;
13047
+ for (const name of Object.keys(scripts)) {
13048
+ if (!SCRIPT_NAME_RE.test(name)) {
13049
+ ctx.addIssue({
13050
+ code: z.ZodIssueCode.custom,
13051
+ path: ["scripts", name],
13052
+ message: `scripts['${name}'] is not a valid script name \u2014 use 1-64 characters of letters, digits, underscores ('_') and hyphens ('-'). The map key IS the name the platform stores and a trigger's scriptRef.name resolves against.`
13053
+ });
13054
+ }
13055
+ }
13056
+ for (const [triggerName, trigger] of Object.entries(bp.triggers ?? {})) {
13057
+ const ref = trigger?.scriptRef;
13058
+ if (!ref?.name) continue;
13059
+ if (!Object.prototype.hasOwnProperty.call(scripts, ref.name)) continue;
13060
+ if (ref.version !== "latest") {
13061
+ ctx.addIssue({
13062
+ code: z.ZodIssueCode.custom,
13063
+ path: ["triggers", triggerName, "scriptRef", "version"],
13064
+ message: `triggers['${triggerName}'].scriptRef pins version '${ref.version}' of '${ref.name}', which is a script THIS blueprint ships \u2014 use version: 'latest' instead. Version numbers are assigned by the platform, so a pin cannot be known to name the source declared here; and once that source is edited the pin would keep running the superseded version. Pin a version only for a script this blueprint does not ship.`
13065
+ });
13066
+ }
13067
+ }
13068
+ }
13069
+ function lintTriggers(value, ctx) {
13070
+ const bp = value;
13071
+ const triggers = bp.triggers;
13072
+ if (!triggers) return;
13073
+ const declaredSchemas = new Set((bp.schemas ?? []).map((s) => s.typeName).filter(Boolean));
13074
+ const triggerEnabledSchemas = new Set(
13075
+ (bp.schemas ?? []).filter((s) => s.capabilities?.triggersEnabled === true).map((s) => s.typeName).filter(Boolean)
13076
+ );
13077
+ const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
13078
+ for (const [name, trigger] of Object.entries(triggers)) {
13079
+ const schemaName = trigger.firingSource?.schemaName;
13080
+ if (schemaName && !declaredSchemas.has(schemaName)) {
13081
+ ctx.addIssue({
13082
+ code: z.ZodIssueCode.custom,
13083
+ path: ["triggers", name, "firingSource", "schemaName"],
13084
+ message: `triggers['${name}'].firingSource.schemaName names '${schemaName}', which is not a schema this blueprint declares in schemas \u2014 ` + (declaredSchemas.size ? `declared schemas: ${[...declaredSchemas].join(", ")}` : "declared schemas: (none)")
13085
+ });
13086
+ } else if (schemaName && !triggerEnabledSchemas.has(schemaName)) {
13087
+ ctx.addIssue({
13088
+ code: z.ZodIssueCode.custom,
13089
+ path: ["triggers", name, "firingSource", "schemaName"],
13090
+ message: `triggers['${name}'].firingSource.schemaName names '${schemaName}', which does not enable triggers \u2014 records of that schema would never fire it. Set capabilities.triggersEnabled: true on the '${schemaName}' schema.`
13091
+ });
13092
+ }
13093
+ const roleIds = trigger.roleIds;
13094
+ if (!roleIds) continue;
13095
+ const seen = /* @__PURE__ */ new Set();
13096
+ roleIds.forEach((roleId, i) => {
13097
+ const path4 = ["triggers", name, "roleIds", i];
13098
+ if (!declaredRoles.has(roleId)) {
13099
+ ctx.addIssue({
13100
+ code: z.ZodIssueCode.custom,
13101
+ path: path4,
13102
+ message: `triggers['${name}'].roleIds names '${roleId}', which is not a role this blueprint declares in roles \u2014 ` + (declaredRoles.size ? `declared roles: ${[...declaredRoles].join(", ")}` : "declared roles: (none)")
13103
+ });
13104
+ }
13105
+ if (seen.has(roleId)) {
13106
+ ctx.addIssue({
13107
+ code: z.ZodIssueCode.custom,
13108
+ path: path4,
13109
+ message: `triggers['${name}'].roleIds names '${roleId}' more than once \u2014 remove the duplicate`
13110
+ });
13111
+ }
13112
+ seen.add(roleId);
13113
+ for (const clause of bp.roles?.[roleId] ?? []) {
13114
+ for (const [dim, raw] of Object.entries(clause?.dataScope ?? {})) {
13115
+ for (const v of Array.isArray(raw) ? raw : [raw]) {
13116
+ if (typeof v !== "string" || !IDENTITY_DERIVED_TOKEN_RE.test(v)) continue;
13117
+ ctx.addIssue({
13118
+ code: z.ZodIssueCode.custom,
13119
+ path: path4,
13120
+ message: `triggers['${name}'].roleIds names '${roleId}', whose dataScope['${dim}'] uses '${v}'. '\${{ self.* }}' / '\${{ under.self.* }}' / '\${{ member.* }}' resolve against the TRIGGER PRINCIPAL's identity, not yours, so the platform refuses them in a trigger grant \u2014 inline or composed. The role itself is fine for ordinary use; it just cannot be the grant for a trigger. Give the trigger an inline grant using '\${{ input.* }}' or literal values, or compose a role without an identity-derived dataScope.`
13121
+ });
13122
+ }
13123
+ }
13124
+ }
13125
+ });
13126
+ }
13127
+ }
11814
13128
  function lintRoleAssumable(value, ctx) {
11815
13129
  const bp = value;
11816
13130
  const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
@@ -11925,7 +13239,21 @@ var BLUEPRINT_FIELD_PHASES = Object.freeze({
11925
13239
  schemas: "in-context",
11926
13240
  accessProfile: "in-context",
11927
13241
  roles: "in-context",
11928
- seed: "in-context"
13242
+ seed: "in-context",
13243
+ // Pushed on the same in-context token as the schemas/roles above, and — load-bearing — BEFORE
13244
+ // `triggers`, because a trigger's `scriptRef` is validated AT DECLARE TIME: a rule naming a script
13245
+ // with no versions in the context is refused with a 400. This map does not order the loader (a
13246
+ // plain object's key order is not a contract); the loader orders itself, and a test pins it.
13247
+ scripts: "in-context",
13248
+ // A trigger's principal + grant are provisioned exactly like the roles: block is (N service-
13249
+ // principal/AccessProfile pairs instead of one), so it applies in the identical in-context phase,
13250
+ // under the same re-minted context-confined token.
13251
+ triggers: "in-context",
13252
+ // Setting it requires the platform provisioning capability, which only the bootstrap credential
13253
+ // carries — the same reasoning as issuers/namespaces above. It is applied to the app context
13254
+ // itself, which the bootstrap phase already creates, so there is nothing for the in-context pass
13255
+ // to do with it.
13256
+ identityProjectionClaims: "bootstrap"
11929
13257
  });
11930
13258
  var BlueprintSchema = z.object({
11931
13259
  /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
@@ -11988,13 +13316,77 @@ var BlueprintSchema = z.object({
11988
13316
  * `allowedSurfaces: ['entity']` schema, both need the registration to
11989
13317
  * already exist). `org`/`client` need no entry — they exist already.
11990
13318
  */
11991
- namespaces: z.array(BlueprintNamespaceSchema).optional()
13319
+ namespaces: z.array(BlueprintNamespaceSchema).optional(),
13320
+ /**
13321
+ * Declares which of the golden IdP-asserted claim names captured by this tenant's issuers (see
13322
+ * `issuers[].capturedClaims`) are projected, read-only, onto access profiles in THIS context.
13323
+ * Applied to the app context in the BOOTSTRAP-token phase, alongside the context's own create.
13324
+ *
13325
+ * Capture and projection are deliberately separate opt-ins: capturing a claim stores it against
13326
+ * the user, projecting it exposes it on a profile in this context. Declaring a name here that no
13327
+ * issuer captures is not an error — it simply never fills.
13328
+ *
13329
+ * <p><b>Fills once, then freezes.</b> A profile's projection is written the first time a sign-in
13330
+ * for that principal can supply a value — usually at profile creation, but for an INVITED member
13331
+ * not until they actually accept and sign in, since there is nothing to project before that.
13332
+ * Once filled it never updates again, even if this declaration or the underlying identity data
13333
+ * changes later. So editing this list affects only profiles that have not been filled yet; it is
13334
+ * not a backfill, and re-applying a blueprint does not re-project existing profiles.
13335
+ *
13336
+ * <p><b>Authority.</b> Setting this needs the platform provisioning capability — the same
13337
+ * authority the bootstrap credential already uses to register issuers and declare namespaces.
13338
+ * An ordinary `app-contexts:u` credential may rename its own context but may not opt it into
13339
+ * projecting IdP-golden identity data, so this field is only settable on the bootstrap phase's
13340
+ * own token, never on the per-context wildcard the in-context load runs under.
13341
+ *
13342
+ * <p>Omit to leave the context's existing declaration unchanged; declare an empty list to
13343
+ * disable future projection. Capped at 20 entries, each a non-blank name of at most 64
13344
+ * characters, duplicates rejected — the same bounds `capturedClaims` carries.
13345
+ *
13346
+ * <p><b>Applies only when this blueprint CREATES the app context.</b> The condition is the
13347
+ * context being created, not the apply being the first one: creating the context separately
13348
+ * beforehand means even the first apply does not set it. Re-applying against a context that
13349
+ * already exists does not change it, in either direction. Edit the declaration on an existing context through the platform API instead. (A
13350
+ * tool applying this blueprint could in principle reconcile it, but doing so also overwrites the
13351
+ * context's name and needs a broader scope — see `@vectros-ai/cli`'s own notes.)
13352
+ */
13353
+ identityProjectionClaims: z.array(
13354
+ z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
13355
+ message: `an identityProjectionClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
13356
+ }).refine((name) => name.trim().length > 0, {
13357
+ message: "an identityProjectionClaims entry must be a non-blank claim name"
13358
+ })
13359
+ ).max(MAX_CAPTURED_CLAIMS, {
13360
+ message: `identityProjectionClaims accepts at most ${MAX_CAPTURED_CLAIMS} entries`
13361
+ }).optional().superRefine(assertNoDuplicateClaimNames("identityProjectionClaims")),
13362
+ /**
13363
+ * Optional scripts this blueprint SHIPS: scriptName -> `{ source, declaredInputContract? }`.
13364
+ *
13365
+ * Distinct from a trigger's `scriptRef`, which only NAMES a script and requires it to already
13366
+ * exist. Pushed BEFORE the `triggers` block, because a `scriptRef` is validated at declare time.
13367
+ *
13368
+ * Each entry is the CURRENT SOURCE for that name, not a version — the platform assigns versions
13369
+ * itself and they are immutable. Re-applying an unchanged blueprint pushes nothing; editing
13370
+ * `source` pushes a new version, which every `version: 'latest'` rule then picks up.
13371
+ */
13372
+ scripts: BlueprintScriptsSchema.optional(),
13373
+ /**
13374
+ * Optional async trigger rules: triggerName -> declaration.
13375
+ *
13376
+ * These FIRE. A record write on a schema that opts in via `capabilities.triggersEnabled`
13377
+ * dispatches the rule's script, which runs asynchronously in a sandbox under the grant declared
13378
+ * here — so a trigger's `allowedActions`/`dataScope`/`capabilities` (or the roles its `roleIds`
13379
+ * composes) are live authority, not a declaration awaiting a future release. Author the grant as
13380
+ * narrowly as you would any other credential's.
13381
+ */
13382
+ triggers: BlueprintTriggersSchema.optional()
11992
13383
  }).strict().superRefine((bp, ctx) => {
11993
13384
  lintSelfTokenPlacement(bp, ctx);
11994
13385
  lintIdentityRefs(bp, ctx);
11995
13386
  lintIdentityOverrides(bp, ctx);
11996
13387
  lintNamespacedScopeArrays(bp, ctx);
11997
13388
  lintDataScopeKeys(bp, ctx);
13389
+ lintReservedFilterableFieldIds(bp, ctx);
11998
13390
  lintExpectedScopeDims(bp, ctx);
11999
13391
  lintDataScopeRefs(bp, ctx);
12000
13392
  lintRoleAssumable(bp, ctx);
@@ -12003,6 +13395,9 @@ var BlueprintSchema = z.object({
12003
13395
  lintIdentitySurfaces(bp, ctx);
12004
13396
  lintIssuerContext(bp, ctx);
12005
13397
  lintNamespaces(bp, ctx);
13398
+ lintTriggers(bp, ctx);
13399
+ lintTriggerFields(bp, ctx);
13400
+ lintScripts(bp, ctx);
12006
13401
  });
12007
13402
  function formatIssuePath(path4) {
12008
13403
  let out = "";
@@ -14087,6 +15482,13 @@ function renderActionableError(info) {
14087
15482
  return ` \u2716 ${msg}
14088
15483
  Your credential lacks the scope for this operation (owner-gated, or a
14089
15484
  narrower context/action than required).
15485
+ `;
15486
+ }
15487
+ if (info.statusCode === 409 && /no scoped key can be minted|is not active|is suspended/i.test(msg)) {
15488
+ return ` \u2716 ${msg}
15489
+ This is NOT a scope problem: no credential, including a root key, can mint against an
15490
+ inactive profile or a suspended user, so re-issuing a broader credential will not help.
15491
+ Follow the reactivation step named above, then retry.
14090
15492
  `;
14091
15493
  }
14092
15494
  if (info.statusCode === 404) {
@@ -14253,7 +15655,10 @@ async function mintBridgeToken(opts) {
14253
15655
  let url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
14254
15656
  if (contextId) url += `&context=${encodeURIComponent(contextId)}`;
14255
15657
  return fetchStToken(url, cognitoToken, fetchImpl, path4, (status) => {
14256
- if (status !== 404 || useScopedToken || contextId) return "";
15658
+ if (status !== 404) return "";
15659
+ if (useScopedToken || contextId) {
15660
+ return " (this route answers every refusal with the same 404: the named context may not exist yet, or your own member account may be suspended in it)";
15661
+ }
14257
15662
  return ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)`;
14258
15663
  });
14259
15664
  }
@@ -14268,7 +15673,10 @@ async function mintContextToken(opts) {
14268
15673
  } = opts;
14269
15674
  const base = apiBaseUrl.replace(/\/$/, "");
14270
15675
  const url = `${base}${SCOPED_TOKEN_PATH}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}&context=${encodeURIComponent(contextId)}`;
14271
- return fetchStToken(url, cognitoToken, fetchImpl, `${SCOPED_TOKEN_PATH}?context=${contextId}`);
15676
+ return fetchStToken(url, cognitoToken, fetchImpl, `${SCOPED_TOKEN_PATH}?context=${contextId}`, (status) => {
15677
+ if (status !== 404) return "";
15678
+ return " (this route answers every refusal with the same 404: the named context may not exist, or your own member account may be suspended in it)";
15679
+ });
14272
15680
  }
14273
15681
 
14274
15682
  // src/identity.ts
@@ -14655,7 +16063,21 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
14655
16063
  // unconditionally, never tenant-wide, so the reach is bounded to what the
14656
16064
  // blueprint already provisions, never the pack's say-so alone. See
14657
16065
  // `@vectros-ai/blueprints`' `namespaces[]` doc for the full design.
14658
- "namespaces"
16066
+ "namespaces",
16067
+ // The 0.43.0 script/trigger surfaces. Listed for MESSAGE QUALITY only — both were already
16068
+ // rejected by the data-plane allowlist above, but as "unrecognized resource", whose text invites
16069
+ // the reader to widen the allowlist ("if 'scripts' is a new data-plane resource…"). They are not:
16070
+ // the platform deliberately put them out of reach of an ordinary confined partner role (a bare,
16071
+ // non-empty `data_scope` clause — the routine blueprint-issued shape — does not reach scripts at
16072
+ // all), because script authoring is a design-time, admin-shaped surface. So the accurate message
16073
+ // is the control-plane one.
16074
+ //
16075
+ // NOTE on the `x` action letter: `scripts:x` (execute) is a real platform verb, and VALID_OPS
16076
+ // deliberately does NOT gain it. Op letters are only checked AFTER a resource is admitted, and
16077
+ // no admitted resource has an `x` verb — adding it would instead make `records:x` pass the op
16078
+ // check and be admitted as data-plane.
16079
+ "scripts",
16080
+ "triggers"
14659
16081
  ];
14660
16082
  var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
14661
16083
  ["profiles", /* @__PURE__ */ new Set(["r", "u", "d"])]
@@ -14663,6 +16085,13 @@ var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
14663
16085
  var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
14664
16086
  ["profiles", /* @__PURE__ */ new Set(["c"])]
14665
16087
  ]);
16088
+ var NAME_QUALIFIED_OPS = /* @__PURE__ */ new Map([
16089
+ ["scripts", /* @__PURE__ */ new Set(["x"])]
16090
+ ]);
16091
+ var RESOURCE_EXTRA_OPS = /* @__PURE__ */ new Map([
16092
+ ["scripts", /* @__PURE__ */ new Set(["x"])]
16093
+ ]);
16094
+ var PLATFORM_SCRIPT_NAME = /^[A-Za-z0-9_-]{1,64}$/;
14666
16095
  var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
14667
16096
  ["profiles", /* @__PURE__ */ new Set(["r"])]
14668
16097
  ]);
@@ -14673,10 +16102,13 @@ var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
14673
16102
  var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
14674
16103
  var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
14675
16104
  var ALLOWLIST_HINT = DATA_PLANE_ALLOWLIST.join("/");
16105
+ function formatRejection(r) {
16106
+ return r.source ? `${r.action} (${r.source}): ${r.reason}` : `${r.action}: ${r.reason}`;
16107
+ }
14676
16108
  var ScopeGateError = class extends Error {
14677
16109
  rejected;
14678
16110
  constructor(rejected) {
14679
- const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
16111
+ const summary = rejected.map((r) => ` \u2022 ${formatRejection(r)}`).join("\n");
14680
16112
  super(
14681
16113
  `Scope gate rejected ${rejected.length} requested scope(s); the bootstrap loader provisions data-plane access only (${ALLOWLIST_HINT}) and mints nothing on reject:
14682
16114
  ${summary}`
@@ -14691,7 +16123,7 @@ var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
14691
16123
  var CapabilityGateError = class extends Error {
14692
16124
  rejected;
14693
16125
  constructor(rejected) {
14694
- const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
16126
+ const summary = rejected.map((r) => ` \u2022 ${formatRejection(r)}`).join("\n");
14695
16127
  super(
14696
16128
  `Capability gate rejected ${rejected.length} requested capability(ies); the bootstrap loader grants (${CAPABILITIES_HINT}) only and mints nothing on reject:
14697
16129
  ${summary}`
@@ -14732,9 +16164,10 @@ function assertCapabilityGate(capabilities) {
14732
16164
  throw new CapabilityGateError(result.rejected);
14733
16165
  }
14734
16166
  }
14735
- function firstInvalidOp(ops) {
16167
+ function firstInvalidOp(ops, resource) {
16168
+ const extra = resource === void 0 ? void 0 : RESOURCE_EXTRA_OPS.get(resource);
14736
16169
  for (const ch of ops) {
14737
- if (!VALID_OPS.has(ch)) return ch;
16170
+ if (!VALID_OPS.has(ch) && !extra?.has(ch)) return ch;
14738
16171
  }
14739
16172
  return void 0;
14740
16173
  }
@@ -14761,19 +16194,24 @@ function evaluateScopeToken(action, capabilities) {
14761
16194
  if (!DATA_PLANE_SET.has(resource)) {
14762
16195
  const admittedOps = PARTIALLY_ADMITTED_OPS.get(resource);
14763
16196
  const selfOnlyOps = SELF_QUALIFIED_OPS.get(resource);
14764
- if (admittedOps || selfOnlyOps) {
14765
- const admittedHint = [.../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? []])].sort().join("/");
16197
+ const nameOnlyOps = NAME_QUALIFIED_OPS.get(resource);
16198
+ if (admittedOps || selfOnlyOps || nameOnlyOps) {
16199
+ const admittedHint = [
16200
+ .../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? [], ...nameOnlyOps ?? []])
16201
+ ].sort().join("/");
14766
16202
  if (ops === "*") {
14767
16203
  return {
14768
16204
  action,
14769
16205
  reason: `requests '${resource}:*', which includes operation(s) outside the narrow allowlist (${admittedHint}) admitted for this control-plane resource. Request only the specific operations you need`
14770
16206
  };
14771
16207
  }
14772
- const invalidOp2 = firstInvalidOp(ops);
16208
+ const invalidOp2 = firstInvalidOp(ops, resource);
14773
16209
  if (invalidOp2) {
16210
+ const extraOps = [...RESOURCE_EXTRA_OPS.get(resource) ?? []].sort();
16211
+ const validHint = ["c", "r", "u", "d", ...extraOps].join(", ");
14774
16212
  return {
14775
16213
  action,
14776
- reason: `invalid operation character '${invalidOp2}' (valid operations are c, r, u, d, or '*')`
16214
+ reason: `invalid operation character '${invalidOp2}' (valid operations are ${validHint}, or '*')`
14777
16215
  };
14778
16216
  }
14779
16217
  const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
@@ -14783,10 +16221,29 @@ function evaluateScopeToken(action, capabilities) {
14783
16221
  if (qualifier !== void 0 && unqualifiableOps?.has(ch)) {
14784
16222
  return {
14785
16223
  action,
14786
- reason: `requests '${resource}:${ch}' alongside a qualifier ('${qualifier}'), but the platform does not resolve a qualifier for '${ch}' on '${resource}' at all \u2014 it will reject the WHOLE entry as malformed, not just ignore the qualifier. Split into separate entries (e.g. '${resource}:${ch}' + the other op(s) with their qualifier).`
16224
+ reason: `requests '${resource}:${ch}' alongside a qualifier ('${qualifier}'), but the platform does not resolve a qualifier for '${ch}' on '${resource}' at all \u2014 it will reject the WHOLE entry as malformed, not just ignore the qualifier. Split into separate entries (e.g. '${resource}:${ch}' + the other op(s) with their qualifier).`
16225
+ };
16226
+ }
16227
+ if (admittedOps?.has(ch)) continue;
16228
+ if (nameOnlyOps?.has(ch)) {
16229
+ if (qualifier !== void 0 && qualifier.includes("*")) {
16230
+ return {
16231
+ action,
16232
+ reason: `requests '${resource}:${ch}:${qualifier}'. Execute grants have NO wildcard \u2014 the qualifier is compared to the script name exactly, so a '*' matches no script and the grant is stored and then refuses every execution, with no error that can say why. Name each script you ship, one entry each: '${resource}:${ch}:<script-name>'`
16233
+ };
16234
+ }
16235
+ if (qualifier !== void 0 && PLATFORM_SCRIPT_NAME.test(qualifier)) continue;
16236
+ if (qualifier !== void 0 && qualifier.length > 0) {
16237
+ return {
16238
+ action,
16239
+ reason: `requests '${resource}:${ch}:${qualifier}', but '${qualifier}' cannot be a script name \u2014 names are 1-64 characters of letters, digits, '_' or '-'. A qualifier that cannot name a script is stored and then matches nothing, so it is refused here rather than at an execution that cannot explain itself`
16240
+ };
16241
+ }
16242
+ return {
16243
+ action,
16244
+ reason: `requests a bare '${resource}:${ch}' with no qualifier. Execution gates on the action verb and the script NAME, and consults no data scope \u2014 so a bare grant means 'may run every script in this context', including ones added later that this blueprint's reviewer never saw. Name each script you ship, one entry each \u2014 '${resource}:${ch}:<script-name>' \u2014 there is no wildcard form.`
14787
16245
  };
14788
16246
  }
14789
- if (admittedOps?.has(ch)) continue;
14790
16247
  if (selfOnlyOps?.has(ch)) {
14791
16248
  if (isSelfQualified) continue;
14792
16249
  const unlockCapability = CAPABILITY_UNLOCKED_OPS.get(resource)?.get(ch);
@@ -14857,6 +16314,50 @@ function assertBlueprintGates(blueprint) {
14857
16314
  assertCapabilityGate(clause.capabilities);
14858
16315
  }
14859
16316
  }
16317
+ for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
16318
+ if (entry.allowedActions === void 0) continue;
16319
+ try {
16320
+ assertScopeGate(entry.allowedActions, entry.capabilities);
16321
+ assertCapabilityGate(entry.capabilities);
16322
+ } catch (err) {
16323
+ throw withTriggerContext(err, triggerName);
16324
+ }
16325
+ }
16326
+ assertScriptGrantsNameADeclaredScript(blueprint);
16327
+ }
16328
+ function assertScriptGrantsNameADeclaredScript(blueprint) {
16329
+ const declared = Object.keys(blueprint.scripts ?? {});
16330
+ if (declared.length === 0) return;
16331
+ const rejected = [];
16332
+ const check = (actions, source) => {
16333
+ for (const action of actions ?? []) {
16334
+ const parts = action.trim().split(":");
16335
+ if (parts[0] !== "scripts" || parts[1] !== "x") continue;
16336
+ const name = parts.length > 2 ? parts.slice(2).join(":") : void 0;
16337
+ if (name === void 0 || name.length === 0 || name.includes("*")) continue;
16338
+ if (!declared.includes(name)) {
16339
+ rejected.push({
16340
+ action,
16341
+ reason: `grants execute on '${name}', which this blueprint does not ship \u2014 it declares ${declared.map((d) => `'${d}'`).join(", ")}. Nothing downstream can catch this: the platform stores the grant, and every execution then fails with a 403 that cannot say the name is wrong. Fix the spelling, or drop the grant if the script is provisioned elsewhere`,
16342
+ ...source ? { source } : {}
16343
+ });
16344
+ }
16345
+ }
16346
+ };
16347
+ check(blueprint.accessProfile.allowedActions);
16348
+ for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
16349
+ for (const clause of clauses) check(clause.allowedActions, `role '${roleId}'`);
16350
+ }
16351
+ for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
16352
+ check(entry.allowedActions, `trigger '${triggerName}'`);
16353
+ }
16354
+ if (rejected.length > 0) throw new ScopeGateError(rejected);
16355
+ }
16356
+ function withTriggerContext(err, triggerName) {
16357
+ if (err instanceof ScopeGateError || err instanceof CapabilityGateError) {
16358
+ for (const r of err.rejected) r.source ??= `trigger '${triggerName}'`;
16359
+ }
16360
+ return err;
14860
16361
  }
14861
16362
  function effectiveAccessProfileActions(blueprint) {
14862
16363
  if (blueprint.accessProfile.allowedActions !== void 0) {
@@ -14968,10 +16469,391 @@ async function resolveApplyTimeIdentities(blueprint, provisioned, bootstrapClien
14968
16469
  const resolved = await resolveBlueprintIdentities(blueprint, resolver);
14969
16470
  return parseBlueprint(resolved);
14970
16471
  }
14971
-
14972
- // src/loader.ts
14973
- function actionOf(created) {
14974
- return created ? "created" : "reconciled";
16472
+
16473
+ // src/scope-grammar.ts
16474
+ var FORBIDDEN_SCOPE_NAMESPACES2 = /* @__PURE__ */ new Set([
16475
+ "user",
16476
+ "record",
16477
+ "document",
16478
+ "entity",
16479
+ "self",
16480
+ "tenant",
16481
+ "context",
16482
+ "scope",
16483
+ "versions",
16484
+ "lookup"
16485
+ ]);
16486
+ var SCOPE_NAMESPACE_RE2 = /^[a-z][a-z0-9_-]{1,31}$/;
16487
+ var SCOPE_NAMESPACE_MIN_LENGTH = 2;
16488
+ var SCOPE_NAMESPACE_MAX_LENGTH = 32;
16489
+ var SCOPE_VALUE_RE2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
16490
+ var SCOPE_VALUE_MIN_LENGTH = 1;
16491
+ var SCOPE_VALUE_MAX_LENGTH = 128;
16492
+ function valueGrammarError(value) {
16493
+ if (!SCOPE_VALUE_RE2.test(value)) {
16494
+ return `'${value}' is not a valid scope value \u2014 ${SCOPE_VALUE_MIN_LENGTH}-${SCOPE_VALUE_MAX_LENGTH} characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'. A value may not contain a colon.`;
16495
+ }
16496
+ return null;
16497
+ }
16498
+ function namespaceGrammarError(ns) {
16499
+ if (FORBIDDEN_SCOPE_NAMESPACES2.has(ns)) {
16500
+ return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
16501
+ }
16502
+ if (!SCOPE_NAMESPACE_RE2.test(ns)) {
16503
+ return `'${ns}' is not a valid namespace \u2014 lowercase, ${SCOPE_NAMESPACE_MIN_LENGTH}-${SCOPE_NAMESPACE_MAX_LENGTH} characters, starting with a letter and continuing with letters, digits, '_' or '-'`;
16504
+ }
16505
+ return null;
16506
+ }
16507
+ function assertScopeEntries(entries, flag) {
16508
+ if (!entries?.length) return;
16509
+ for (const raw of entries) {
16510
+ const sep = raw.indexOf(":");
16511
+ if (sep <= 0 || sep === raw.length - 1) {
16512
+ throw new Error(`${flag} must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
16513
+ }
16514
+ const nsError = namespaceGrammarError(raw.slice(0, sep));
16515
+ if (nsError) throw new Error(`${flag} namespace ${nsError}`);
16516
+ const valError = valueGrammarError(raw.slice(sep + 1));
16517
+ if (valError) throw new Error(`${flag} value ${valError}`);
16518
+ }
16519
+ }
16520
+ function assertIdentityOverrideValues(overrides, flag) {
16521
+ if (!overrides) return;
16522
+ for (const [key, value] of Object.entries(overrides)) {
16523
+ if (!key.startsWith("scope:")) continue;
16524
+ const error = valueGrammarError(typeof value === "string" ? value : String(value));
16525
+ if (error) throw new Error(`${flag} '${key}' value ${error}`);
16526
+ }
16527
+ }
16528
+ function placeholderScopeNamespace(value) {
16529
+ const self2 = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
16530
+ if (self2) return self2[1];
16531
+ const member = MEMBER_SCOPE_PLACEHOLDER_RE2.exec(value);
16532
+ return member ? member[1] : null;
16533
+ }
16534
+ var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
16535
+ var UNDER_SELF_PLACEHOLDER_RE2 = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
16536
+ var MEMBER_SCOPE_PLACEHOLDER_RE2 = /^\$\{\{\s*member\.scope\.([a-z][a-z0-9_-]{1,31})(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
16537
+ var ROLE_ID_RE2 = /^[a-z][a-z0-9-]{2,30}$/;
16538
+ var MAX_ASSIGNABLE_ROLES2 = 20;
16539
+ function parseAssignableRoles(raw, flag = "--assignable-role") {
16540
+ if (!raw || raw.length === 0) return void 0;
16541
+ if (raw.length > MAX_ASSIGNABLE_ROLES2) {
16542
+ throw new Error(`${flag} accepts at most ${MAX_ASSIGNABLE_ROLES2} entries; got ${raw.length}.`);
16543
+ }
16544
+ const seen = /* @__PURE__ */ new Set();
16545
+ for (const roleId of raw) {
16546
+ if (!ROLE_ID_RE2.test(roleId)) {
16547
+ throw new Error(
16548
+ `${flag} '${roleId}' is not a valid roleId \u2014 3-31 characters, starting with a lowercase letter and continuing with lowercase letters, digits or '-'.`
16549
+ );
16550
+ }
16551
+ if (seen.has(roleId)) {
16552
+ throw new Error(`${flag} repeats '${roleId}' \u2014 list it once.`);
16553
+ }
16554
+ seen.add(roleId);
16555
+ }
16556
+ return [...raw];
16557
+ }
16558
+
16559
+ // src/verb-support.ts
16560
+ async function withAuth(opts, deps, body) {
16561
+ const log = deps.log ?? createLogger2();
16562
+ const resolveAuth = deps.resolveAuth ?? resolveAuthContext;
16563
+ let auth;
16564
+ try {
16565
+ auth = await resolveAuth({
16566
+ env: opts.env,
16567
+ baseUrl: opts.baseUrl,
16568
+ token: opts.token,
16569
+ context: opts.context,
16570
+ provisioning: opts.provisioning,
16571
+ tenant: opts.tenant,
16572
+ useScopedToken: opts.useScopedToken,
16573
+ log
16574
+ });
16575
+ } catch (err) {
16576
+ if (err instanceof MissingCredentialError) {
16577
+ process.stderr.write(` \u2716 ${err.message}
16578
+ `);
16579
+ return 2;
16580
+ }
16581
+ process.stderr.write(` \u2716 ${formatError(err)}
16582
+ `);
16583
+ return 1;
16584
+ }
16585
+ return body(auth, log);
16586
+ }
16587
+ function requireTenantId(auth) {
16588
+ if (!auth.tenantId) {
16589
+ throw new Error(
16590
+ "Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed."
16591
+ );
16592
+ }
16593
+ return auth.tenantId;
16594
+ }
16595
+ function normalizePrincipalUserId(principal) {
16596
+ return principal.startsWith("usr_") ? principal.slice("usr_".length) : principal;
16597
+ }
16598
+ function splitCsv(csv) {
16599
+ return csv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
16600
+ }
16601
+ function parseScopeOverrides(scopes) {
16602
+ if (!scopes || scopes.length === 0) return void 0;
16603
+ const out = {};
16604
+ for (const raw of scopes) {
16605
+ const sep = raw.indexOf(":");
16606
+ if (sep <= 0 || sep === raw.length - 1) {
16607
+ throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
16608
+ }
16609
+ const ns = raw.slice(0, sep);
16610
+ const value = raw.slice(sep + 1);
16611
+ const valueError = valueGrammarError(value);
16612
+ if (valueError) throw new Error(`--scope value ${valueError}`);
16613
+ const key = `scope:${ns}`;
16614
+ if (Object.prototype.hasOwnProperty.call(out, key)) {
16615
+ throw new Error(`--scope sets namespace '${ns}' more than once \u2014 a scope override takes at most one value per namespace.`);
16616
+ }
16617
+ out[key] = value;
16618
+ }
16619
+ if (Object.keys(out).length > 2) {
16620
+ throw new Error("--scope allows at most two distinct namespaces.");
16621
+ }
16622
+ return out;
16623
+ }
16624
+ function parseJsonObjectFlag(flag, raw) {
16625
+ let parsed;
16626
+ try {
16627
+ parsed = JSON.parse(raw);
16628
+ } catch {
16629
+ throw new Error(`${flag} is not valid JSON: ${raw}`);
16630
+ }
16631
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
16632
+ throw new Error(`${flag} must be a JSON object (e.g. '{"userId":["<id>"]}').`);
16633
+ }
16634
+ return parsed;
16635
+ }
16636
+
16637
+ // src/paginate.ts
16638
+ var PAGE_LIMIT = 100;
16639
+ async function drainPages(fetch2) {
16640
+ const out = [];
16641
+ let startFrom;
16642
+ for (; ; ) {
16643
+ const page = await fetch2(startFrom);
16644
+ if (page.data) out.push(...page.data);
16645
+ const next = page.nextCursor;
16646
+ if (!next) break;
16647
+ startFrom = next;
16648
+ }
16649
+ return out;
16650
+ }
16651
+
16652
+ // src/loader.ts
16653
+ function actionOf(created) {
16654
+ return created ? "created" : "reconciled";
16655
+ }
16656
+ function triggerPrincipalExternalId(blueprintName, triggerName) {
16657
+ return `${blueprintName}-trigger-${triggerName}`;
16658
+ }
16659
+ var SCRIPT_CONFLICT_RETRY_DELAY_MS = 750;
16660
+ function isVersionConflict(err) {
16661
+ return typeof err === "object" && err !== null && err.statusCode === 409;
16662
+ }
16663
+ async function readLatestScriptVersion(ctxClient, name) {
16664
+ const versions = await drainPages(
16665
+ (startFrom) => ctxClient.scripts.listScripts({ name, startFrom, limit: PAGE_LIMIT })
16666
+ );
16667
+ return versions.reduce((best, row) => {
16668
+ if (row.scriptVersion === void 0) return best;
16669
+ if (best === void 0 || best.scriptVersion === void 0) return row;
16670
+ return row.scriptVersion > best.scriptVersion ? row : best;
16671
+ }, void 0);
16672
+ }
16673
+ function requireScriptId(row, name) {
16674
+ if (!row.id) {
16675
+ throw new Error(`Script '${name}' listing returned a version with no id.`);
16676
+ }
16677
+ return row.id;
16678
+ }
16679
+ function scriptMatches(stored, declared) {
16680
+ return stored.source === declared.source && (stored.declaredInputContract ?? void 0) === declared.declaredInputContract;
16681
+ }
16682
+ async function ensureScripts(args) {
16683
+ const { ctxClient, scripts, log } = args;
16684
+ const actions = [];
16685
+ for (const [name, entry] of Object.entries(scripts)) {
16686
+ let latest = await readLatestScriptVersion(ctxClient, name);
16687
+ if (latest !== void 0 && scriptMatches(latest, entry)) {
16688
+ actions.push({
16689
+ name,
16690
+ action: "unchanged",
16691
+ id: requireScriptId(latest, name),
16692
+ scriptVersion: latest.scriptVersion ?? 0
16693
+ });
16694
+ log.debug(
16695
+ { component: "loader", scriptName: name, scriptVersion: latest.scriptVersion },
16696
+ "script already matches the declared source \u2014 no version pushed"
16697
+ );
16698
+ continue;
16699
+ }
16700
+ const body = { name, source: entry.source, declaredInputContract: entry.declaredInputContract };
16701
+ let pushed;
16702
+ try {
16703
+ pushed = await ctxClient.scripts.createScript(body);
16704
+ } catch (err) {
16705
+ if (!isVersionConflict(err)) throw err;
16706
+ log.debug(
16707
+ { component: "loader", scriptName: name },
16708
+ "concurrent push detected (409) \u2014 pausing, then re-reading the current version"
16709
+ );
16710
+ await new Promise((resolve) => setTimeout(resolve, SCRIPT_CONFLICT_RETRY_DELAY_MS));
16711
+ try {
16712
+ latest = await readLatestScriptVersion(ctxClient, name);
16713
+ } catch (readErr) {
16714
+ throw new Error(
16715
+ `Script '${name}': a concurrent push was detected (409) and the follow-up read to resolve it also failed (${readErr instanceof Error ? readErr.message : String(readErr)}). Re-run the apply once the other write has settled.`
16716
+ );
16717
+ }
16718
+ if (latest !== void 0 && scriptMatches(latest, entry)) {
16719
+ actions.push({
16720
+ name,
16721
+ action: "unchanged",
16722
+ id: requireScriptId(latest, name),
16723
+ scriptVersion: latest.scriptVersion ?? 0
16724
+ });
16725
+ log.debug(
16726
+ { component: "loader", scriptName: name, scriptVersion: latest.scriptVersion },
16727
+ "the concurrent push stored the same source \u2014 converged, nothing to do"
16728
+ );
16729
+ continue;
16730
+ }
16731
+ try {
16732
+ pushed = await ctxClient.scripts.createScript(body);
16733
+ } catch (retryErr) {
16734
+ if (!isVersionConflict(retryErr)) throw retryErr;
16735
+ throw new Error(
16736
+ `Script '${name}': still conflicting (409) after a re-read and retry. Another apply is pushing this script name concurrently \u2014 re-run once it has finished.`
16737
+ );
16738
+ }
16739
+ }
16740
+ if (!pushed.id) {
16741
+ throw new Error(`Script '${name}' push returned no id.`);
16742
+ }
16743
+ const firstVersion = latest === void 0;
16744
+ actions.push({
16745
+ name,
16746
+ action: firstVersion ? "created" : "superseded",
16747
+ id: pushed.id,
16748
+ scriptVersion: pushed.scriptVersion ?? 0
16749
+ });
16750
+ log.debug(
16751
+ { component: "loader", scriptName: name, scriptVersion: pushed.scriptVersion },
16752
+ firstVersion ? "script pushed \u2014 first version for this name in this context" : "script source changed \u2014 new version pushed over an existing name"
16753
+ );
16754
+ }
16755
+ return actions;
16756
+ }
16757
+ async function pruneUndeclaredTriggers(args) {
16758
+ const { ctxClient, blueprintName, contextId, declaredNames, log } = args;
16759
+ const pruned = [];
16760
+ const skipped = [];
16761
+ let live;
16762
+ try {
16763
+ live = await drainPages(
16764
+ (startFrom) => ctxClient.triggers.listTriggers({ startFrom, limit: PAGE_LIMIT })
16765
+ );
16766
+ } catch (err) {
16767
+ log.warn(
16768
+ { component: "loader", err: extractServerError(err) },
16769
+ "could not list trigger rules \u2014 skipping the trigger reconcile entirely; a trigger removed from this blueprint may still be live"
16770
+ );
16771
+ return { pruned, skipped: [{ reason: "the context's trigger rules could not be listed" }] };
16772
+ }
16773
+ for (const row of live) {
16774
+ if (!row.name || !row.id || !row.principalId) {
16775
+ if (!declaredNames.has(row.name ?? "")) {
16776
+ skipped.push({
16777
+ name: row.name,
16778
+ reason: "the trigger rule carries no id/name/principal, so it cannot be safely de-provisioned"
16779
+ });
16780
+ }
16781
+ continue;
16782
+ }
16783
+ if (declaredNames.has(row.name)) continue;
16784
+ if (!row.provisionedBy) {
16785
+ skipped.push({
16786
+ name: row.name,
16787
+ reason: "it carries no blueprint provenance marker, so it cannot be attributed to this blueprint"
16788
+ });
16789
+ continue;
16790
+ }
16791
+ if (row.provisionedBy !== blueprintName) {
16792
+ log.debug(
16793
+ { component: "loader", triggerName: row.name, provisionedBy: row.provisionedBy },
16794
+ "undeclared trigger left untouched \u2014 provisioned by a different blueprint"
16795
+ );
16796
+ continue;
16797
+ }
16798
+ let principalExternalId;
16799
+ try {
16800
+ principalExternalId = (await ctxClient.identity.getUser({ id: normalizePrincipalUserId(row.principalId) })).externalId;
16801
+ } catch (err) {
16802
+ log.warn(
16803
+ { component: "loader", triggerName: row.name, principalId: row.principalId, err: extractServerError(err) },
16804
+ "could not resolve a trigger's principal \u2014 leaving the trigger in place"
16805
+ );
16806
+ skipped.push({
16807
+ name: row.name,
16808
+ reason: "its service principal could not be read, so ownership could not be fully established"
16809
+ });
16810
+ continue;
16811
+ }
16812
+ if (principalExternalId !== triggerPrincipalExternalId(blueprintName, row.name)) {
16813
+ log.warn(
16814
+ { component: "loader", triggerName: row.name, principalId: row.principalId },
16815
+ "undeclared trigger left untouched \u2014 it carries this blueprint's provenance marker but names a principal this blueprint did not mint for it"
16816
+ );
16817
+ skipped.push({
16818
+ name: row.name,
16819
+ reason: "it names a service principal this blueprint did not mint for a trigger of that name, so its provenance is inconsistent"
16820
+ });
16821
+ continue;
16822
+ }
16823
+ let profileDeleted = true;
16824
+ try {
16825
+ await ctxClient.auth.deleteAccessProfile({ contextId, principalId: row.principalId });
16826
+ } catch (err) {
16827
+ if (!isNotFound(err)) {
16828
+ log.warn(
16829
+ { component: "loader", triggerName: row.name, principalId: row.principalId, err: extractServerError(err) },
16830
+ "could not delete a removed trigger's access profile \u2014 leaving the trigger rule in place so the next apply retries"
16831
+ );
16832
+ skipped.push({ name: row.name, reason: "its access profile could not be deleted, so its grant may still be live" });
16833
+ continue;
16834
+ }
16835
+ profileDeleted = false;
16836
+ }
16837
+ try {
16838
+ await ctxClient.triggers.deleteTrigger({ id: row.id });
16839
+ } catch (err) {
16840
+ log.warn(
16841
+ { component: "loader", triggerName: row.name, err: extractServerError(err) },
16842
+ "a removed trigger's access profile was deleted but the rule itself was not \u2014 it can no longer execute, and the next apply will retry the rule"
16843
+ );
16844
+ skipped.push({ name: row.name, reason: "its grant was removed but the rule row itself could not be deleted" });
16845
+ continue;
16846
+ }
16847
+ pruned.push({ name: row.name, principalId: row.principalId, profileDeleted });
16848
+ log.debug(
16849
+ { component: "loader", triggerName: row.name, principalId: row.principalId, profileDeleted },
16850
+ "undeclared trigger de-provisioned (no longer declared by this blueprint)"
16851
+ );
16852
+ }
16853
+ return { pruned, skipped };
16854
+ }
16855
+ function isNotFound(err) {
16856
+ return typeof err === "object" && err !== null && err.statusCode === 404;
14975
16857
  }
14976
16858
  async function registerNamespaceIdempotently(client, blueprintContextId, decl) {
14977
16859
  const contextIdForRequest = decl.tenantWide ? void 0 : blueprintContextId;
@@ -15075,6 +16957,10 @@ function toSdkFields(blueprint, schemaIndex) {
15075
16957
  required: f.required,
15076
16958
  searchable: f.searchable,
15077
16959
  filterable: f.filterable,
16960
+ // Forwarded explicitly, like every sibling here: this mapping is a named-property list, not a
16961
+ // spread, so an unforwarded field is dropped SILENTLY rather than failing — which is what
16962
+ // happened to `validation` before it was added below.
16963
+ inline: f.inline,
15078
16964
  description: f.description,
15079
16965
  enumValues: mapEnumValues(f.enumValues),
15080
16966
  // Format passthrough — the loader previously DROPPED this (the
@@ -15130,13 +17016,16 @@ function toCapabilities(capabilities) {
15130
17016
  if (!capabilities) return void 0;
15131
17017
  const out = {};
15132
17018
  if (capabilities.auditHistory !== void 0) out.auditHistory = capabilities.auditHistory;
17019
+ if (capabilities.triggersEnabled !== void 0)
17020
+ out.triggersEnabled = capabilities.triggersEnabled;
15133
17021
  return Object.keys(out).length > 0 ? out : void 0;
15134
17022
  }
15135
17023
  function toRoleScopeClauses(clauses) {
15136
17024
  return clauses.map((c) => ({
15137
17025
  allowed_actions: c.allowedActions,
15138
17026
  data_scope: c.dataScope,
15139
- granted_capabilities: c.capabilities
17027
+ granted_capabilities: c.capabilities,
17028
+ assignable_roles: c.assignableRoles
15140
17029
  }));
15141
17030
  }
15142
17031
  var UnresolvedIdentitiesError = class extends Error {
@@ -15173,7 +17062,13 @@ async function provisionContext(client, input) {
15173
17062
  const { blueprint, allowTenantWideNamespaces = false, readExistingKey, confirmExistingPrincipal, log } = input;
15174
17063
  assertBlueprintGates(blueprint);
15175
17064
  assertTenantWideNamespacesAllowed(blueprint, allowTenantWideNamespaces);
15176
- const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
17065
+ const ctxResp = await client.auth.createAppContext({
17066
+ body: {
17067
+ contextId: blueprint.contextId,
17068
+ name: contextNameOf(blueprint),
17069
+ ...blueprint.identityProjectionClaims === void 0 ? {} : { identityProjectionClaims: blueprint.identityProjectionClaims }
17070
+ }
17071
+ });
15177
17072
  const contextAction = actionOf(ctxResp.created);
15178
17073
  log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
15179
17074
  const user = await client.identity.createUser({
@@ -15260,7 +17155,8 @@ async function runLoader(client, input) {
15260
17155
  subClaim: issuer.subClaim,
15261
17156
  emailClaim: issuer.emailClaim,
15262
17157
  userinfoUri: issuer.userinfoUri,
15263
- selfSignupPolicies
17158
+ selfSignupPolicies,
17159
+ capturedClaims: issuer.capturedClaims
15264
17160
  });
15265
17161
  const action = actionOf(issuerResp.created);
15266
17162
  issuerActions.push({ issuerId: issuer.issuerId, action });
@@ -15276,7 +17172,8 @@ async function runLoader(client, input) {
15276
17172
  subClaim: issuer.subClaim,
15277
17173
  emailClaim: issuer.emailClaim,
15278
17174
  userinfoUri: issuer.userinfoUri,
15279
- selfSignupPolicies
17175
+ selfSignupPolicies,
17176
+ capturedClaims: issuer.capturedClaims
15280
17177
  });
15281
17178
  log.debug({ component: "loader", issuerId: issuer.issuerId }, "issuer already registered \u2014 reconciled safe fields to the declared values");
15282
17179
  } else {
@@ -15357,7 +17254,10 @@ async function runLoader(client, input) {
15357
17254
  data_scope: blueprint.accessProfile.dataScope,
15358
17255
  // Already gate-validated (assertBlueprintGates, above this function's
15359
17256
  // call site) before this body is ever built.
15360
- granted_capabilities: blueprint.accessProfile.capabilities
17257
+ granted_capabilities: blueprint.accessProfile.capabilities,
17258
+ // Absent means "no roleId restriction" server-side; an empty list is refused. Undeclared
17259
+ // must therefore stay `undefined` rather than becoming `[]`.
17260
+ assignable_roles: blueprint.accessProfile.assignableRoles
15361
17261
  }
15362
17262
  ],
15363
17263
  // Optional identity overrides — scope values the service key STAMPS onto
@@ -15380,6 +17280,109 @@ async function runLoader(client, input) {
15380
17280
  body: profileBody
15381
17281
  });
15382
17282
  log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
17283
+ const scriptActions = blueprint.scripts ? await ensureScripts({ ctxClient, scripts: blueprint.scripts, log }) : [];
17284
+ const triggerActions = [];
17285
+ for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
17286
+ const schemaId = schemaIds[entry.firingSource.schemaName];
17287
+ if (!schemaId) {
17288
+ throw new Error(
17289
+ `Trigger '${triggerName}' names schema '${entry.firingSource.schemaName}', which has no resolved schemaId \u2014 the schema failed to provision.`
17290
+ );
17291
+ }
17292
+ const triggerUser = await client.identity.createUser({
17293
+ body: {
17294
+ externalId: triggerPrincipalExternalId(blueprint.name, triggerName),
17295
+ type: "SERVICE",
17296
+ payload: { displayName: `Trigger: ${triggerName}` }
17297
+ }
17298
+ });
17299
+ const triggerUserId = triggerUser.id;
17300
+ if (!triggerUserId) {
17301
+ throw new Error(`Trigger '${triggerName}' service-principal create returned no user id.`);
17302
+ }
17303
+ const triggerPlaceholderProfileBody = {
17304
+ principalId: `usr_${triggerUserId}`,
17305
+ scopes: [{ allowed_actions: ["triggers:r"] }]
17306
+ };
17307
+ const triggerProfile = await ctxClient.auth.createAccessProfile({
17308
+ contextId: blueprint.contextId,
17309
+ body: triggerPlaceholderProfileBody
17310
+ });
17311
+ log.debug(
17312
+ { component: "loader", triggerName, principalId: `usr_${triggerUserId}`, accessProfileId: triggerProfile.id },
17313
+ "trigger principal + placeholder access profile ensured (no declared grant attached yet)"
17314
+ );
17315
+ const triggerResp = await ctxClient.triggers.createTrigger({
17316
+ upsert: true,
17317
+ body: {
17318
+ name: triggerName,
17319
+ firingSource: { schemaId, event: entry.firingSource.event },
17320
+ scriptRef: entry.scriptRef,
17321
+ roleIds: entry.roleIds,
17322
+ scopes: entry.roleIds ? void 0 : [
17323
+ {
17324
+ allowed_actions: entry.allowedActions,
17325
+ data_scope: entry.dataScope,
17326
+ granted_capabilities: entry.capabilities
17327
+ }
17328
+ ],
17329
+ manifest: entry.manifest,
17330
+ // REQUIRED on create. `[]` is a real declaration ("project nothing beyond input.recordId"),
17331
+ // not an omission, so it is forwarded as-is rather than collapsed to undefined the way an
17332
+ // absent optional would be — the blueprint schema makes it required precisely so an author
17333
+ // STATES it.
17334
+ fields: entry.fields,
17335
+ principalId: `usr_${triggerUserId}`,
17336
+ // The PROVENANCE MARKER the reconcile-by-absence pass gates its deletes on. Stamped on every
17337
+ // apply (not only the first) so a trigger provisioned before this field existed acquires one
17338
+ // the next time its blueprint is applied — the platform accepts a set-when-absent and refuses
17339
+ // a CHANGE, so re-stating it is idempotent and can never re-point someone else's trigger.
17340
+ provisionedBy: blueprint.name
17341
+ }
17342
+ });
17343
+ triggerActions.push({ name: triggerName, action: actionOf(triggerResp.created) });
17344
+ log.debug({ component: "loader", triggerName, action: actionOf(triggerResp.created) }, "trigger rule ensured (upsert)");
17345
+ const triggerProfileBody = entry.roleIds ? { principalId: `usr_${triggerUserId}`, roleIds: entry.roleIds } : {
17346
+ principalId: `usr_${triggerUserId}`,
17347
+ scopes: [
17348
+ {
17349
+ // Non-null: the `else` branch of the roleIds XOR above — BlueprintTriggerEntrySchema
17350
+ // guarantees allowedActions is set whenever roleIds is not.
17351
+ allowed_actions: entry.allowedActions,
17352
+ data_scope: entry.dataScope,
17353
+ granted_capabilities: entry.capabilities
17354
+ }
17355
+ ]
17356
+ };
17357
+ await ctxClient.auth.updateAccessProfile({
17358
+ contextId: blueprint.contextId,
17359
+ principalId: `usr_${triggerUserId}`,
17360
+ body: triggerProfileBody
17361
+ });
17362
+ log.debug(
17363
+ { component: "loader", triggerName, principalId: `usr_${triggerUserId}` },
17364
+ "trigger access profile widened to its declared grant"
17365
+ );
17366
+ }
17367
+ let triggerPrunes = [];
17368
+ let triggerPruneSkipped = [];
17369
+ if (input.pruneTriggers === false) {
17370
+ log.debug({ component: "loader" }, "trigger reconcile-by-absence skipped \u2014 disabled by the caller");
17371
+ } else if (!input.remintForContext) {
17372
+ log.warn(
17373
+ { component: "loader", contextId: blueprint.contextId },
17374
+ "trigger reconcile-by-absence skipped \u2014 no per-context client is wired, so a removed trigger may still be live"
17375
+ );
17376
+ triggerPruneSkipped = [{ reason: "no per-context client was wired, so triggers could not be reconciled in the blueprint's own context" }];
17377
+ } else {
17378
+ ({ pruned: triggerPrunes, skipped: triggerPruneSkipped } = await pruneUndeclaredTriggers({
17379
+ ctxClient,
17380
+ blueprintName: blueprint.name,
17381
+ contextId: blueprint.contextId,
17382
+ declaredNames: new Set(Object.keys(blueprint.triggers ?? {})),
17383
+ log
17384
+ }));
17385
+ }
15383
17386
  const keyName = keyNameFor(blueprint.name, machineId);
15384
17387
  let keyAction = "created";
15385
17388
  let revokedKeyId;
@@ -15501,6 +17504,10 @@ async function runLoader(client, input) {
15501
17504
  profileAction,
15502
17505
  schemaActions,
15503
17506
  roleActions,
17507
+ scriptActions,
17508
+ triggerActions,
17509
+ triggerPrunes,
17510
+ triggerPruneSkipped,
15504
17511
  issuerActions,
15505
17512
  namespaceActions,
15506
17513
  rawKey,
@@ -15521,7 +17528,11 @@ function renderPlanDataScope(ds) {
15521
17528
  }
15522
17529
  function buildPlan(blueprint, opts = {}) {
15523
17530
  const steps = [];
15524
- steps.push({ kind: "context", summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)})` });
17531
+ const projection = blueprint.identityProjectionClaims?.length ? `, identity projection: [${blueprint.identityProjectionClaims.join(", ")}]` : blueprint.identityProjectionClaims ? ", identity projection: disabled (empty list)" : "";
17532
+ steps.push({
17533
+ kind: "context",
17534
+ summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)}${projection})`
17535
+ });
15525
17536
  steps.push({
15526
17537
  kind: "principal",
15527
17538
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
@@ -15537,9 +17548,10 @@ function buildPlan(blueprint, opts = {}) {
15537
17548
  }
15538
17549
  for (const iss of blueprint.issuers ?? []) {
15539
17550
  const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
17551
+ const captured = iss.capturedClaims?.length ? `, captured claims: [${iss.capturedClaims.join(", ")}]` : "";
15540
17552
  steps.push({
15541
17553
  kind: "issuer",
15542
- summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
17554
+ summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}${captured}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
15543
17555
  });
15544
17556
  }
15545
17557
  for (const s of blueprint.schemas) {
@@ -15561,7 +17573,8 @@ function buildPlan(blueprint, opts = {}) {
15561
17573
  const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
15562
17574
  const clauseLines = clauses.map((c, i) => {
15563
17575
  const capNote = c.capabilities?.length ? `, capabilities [${c.capabilities.join(", ")}]` : "";
15564
- return ` clause ${i + 1}: actions [${c.allowedActions.join(", ")}], dataScope: ${renderPlanDataScope(c.dataScope)}${capNote}`;
17576
+ const assignableNote = c.assignableRoles?.length ? `, assignableRoles [${c.assignableRoles.join(", ")}]` : "";
17577
+ return ` clause ${i + 1}: actions [${c.allowedActions.join(", ")}], dataScope: ${renderPlanDataScope(c.dataScope)}${capNote}${assignableNote}`;
15565
17578
  });
15566
17579
  const assumable = blueprint.roleAssumable?.[roleId];
15567
17580
  const assumableLine = assumable ? `
@@ -15591,6 +17604,43 @@ function buildPlan(blueprint, opts = {}) {
15591
17604
  summary: `profile scopes [${actions.join(", ")}]${planeNote}${scopeNote}${capabilityNote}`
15592
17605
  });
15593
17606
  }
17607
+ for (const [scriptName, entry] of Object.entries(blueprint.scripts ?? {})) {
17608
+ const bytes = new TextEncoder().encode(entry.source).length;
17609
+ const contractNote = entry.declaredInputContract ? " + input contract" : "";
17610
+ steps.push({
17611
+ kind: "script",
17612
+ summary: `script '${scriptName}' (ensure latest matches ${bytes} bytes of source${contractNote}) [${BLUEPRINT_FIELD_PHASES.scripts}-token phase]`
17613
+ });
17614
+ }
17615
+ for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
17616
+ steps.push({
17617
+ kind: "principal",
17618
+ summary: `trigger '${triggerName}' principal '${blueprint.name}-trigger-${triggerName}' (SERVICE)`
17619
+ });
17620
+ steps.push({
17621
+ kind: "profile",
17622
+ summary: `trigger '${triggerName}' profile: minimal placeholder grant (minted before the trigger row exists)`
17623
+ });
17624
+ const manifestNote = entry.manifest?.length ? ` manifest: [${entry.manifest.join(", ")}]` : " manifest: (none \u2014 full grant applies)";
17625
+ const fieldsNote = entry.fields?.length ? ` projects: [${entry.fields.join(", ")}]` : " projects: (nothing beyond the record id)";
17626
+ steps.push({
17627
+ kind: "trigger",
17628
+ summary: `trigger '${triggerName}': ${entry.firingSource.schemaName}.${entry.firingSource.event} \u2192 ${entry.scriptRef.name}@${entry.scriptRef.version}${fieldsNote}${manifestNote} [${BLUEPRINT_FIELD_PHASES.triggers}-token phase]`
17629
+ });
17630
+ if (entry.roleIds) {
17631
+ steps.push({
17632
+ kind: "profile",
17633
+ summary: `trigger '${triggerName}' profile widened via role(s) [${entry.roleIds.join(", ")}] (composed)`
17634
+ });
17635
+ } else {
17636
+ const scopeNote = entry.dataScope && Object.keys(entry.dataScope).length ? ` dataScope: ${Object.keys(entry.dataScope).join(", ")}` : "";
17637
+ const capabilityNote = entry.capabilities?.length ? ` capabilities: [${entry.capabilities.join(", ")}]` : "";
17638
+ steps.push({
17639
+ kind: "profile",
17640
+ summary: `trigger '${triggerName}' profile widened to declared scopes [${(entry.allowedActions ?? []).join(", ")}]${scopeNote}${capabilityNote}`
17641
+ });
17642
+ }
17643
+ }
15594
17644
  steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
15595
17645
  const seeds = blueprint.seed ?? [];
15596
17646
  if (seeds.length > 0 && !opts.noSeed) {
@@ -15606,9 +17656,27 @@ function buildPlan(blueprint, opts = {}) {
15606
17656
  function renderPlan(blueprint, steps) {
15607
17657
  const header = ` Plan for blueprint '${blueprint.name}' (v${blueprint.version}) \u2192 context '${blueprint.contextId}':`;
15608
17658
  const legendLines = [
15609
- ` Apply is a non-destructive reconcile \u2014 each item is created if absent, else`,
15610
- ` updated in place to match. Nothing is dropped or recreated.`
17659
+ ` Apply is a reconcile \u2014 each item is created if absent, else updated in place to`,
17660
+ ` match. Nothing is recreated, and nothing is dropped except as noted below.`
15611
17661
  ];
17662
+ legendLines.push(
17663
+ ` Exception: triggers are also reconciled by ABSENCE \u2014 a trigger this blueprint`,
17664
+ ` previously provisioned and no longer declares is DELETED (the rule and its access`,
17665
+ ` profile; its service principal is kept). This applies even when this plan shows no`,
17666
+ ` trigger steps: removing the last one is exactly what leaves none to show. Triggers`,
17667
+ ` created outside this blueprint are never touched. Each deletion is named in the`,
17668
+ ` apply output.`
17669
+ );
17670
+ legendLines.push(
17671
+ ` Counter-exception: a script is never updated in place \u2014 a changed source ADDS a new`,
17672
+ ` immutable version and every earlier one persists, which is also how a 'latest' rule`,
17673
+ ` picks the change up. And scripts are NOT reconciled by absence: a script this blueprint`,
17674
+ ` previously shipped and no longer declares is LEFT IN PLACE, including every stored`,
17675
+ ` version. A script row carries no provisioning marker, so an apply cannot tell one it`,
17676
+ ` created from one you pushed by hand \u2014 and a wrong delete is unrecoverable and would`,
17677
+ ` break any trigger still referencing it. Remove one deliberately with`,
17678
+ ` 'vectros scripts delete', after 'vectros scripts list' shows you what is there.`
17679
+ );
15612
17680
  if (steps.some((s) => s.kind === "issuer")) {
15613
17681
  legendLines.push(
15614
17682
  ` Exception: an issuer's trust-anchor fields (issuer, jwksUri, audience,`,
@@ -17860,6 +19928,19 @@ function namespaceTally(items) {
17860
19928
  if (matched) seg.push(`${matched} already registered \u2014 confirmed matching`);
17861
19929
  return `${items.length} namespace${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17862
19930
  }
19931
+ function scriptTally(items) {
19932
+ if (items.length === 0) return null;
19933
+ const created = items.filter((i) => i.action === "created");
19934
+ const superseded = items.filter((i) => i.action === "superseded");
19935
+ const unchanged = items.length - created.length - superseded.length;
19936
+ const seg = [];
19937
+ if (created.length) seg.push(`${created.length} first version${created.length === 1 ? "" : "s"} pushed`);
19938
+ if (superseded.length) {
19939
+ seg.push(`${superseded.map((i) => `'${i.name}'`).join(", ")} superseded (a new version now current)`);
19940
+ }
19941
+ if (unchanged) seg.push(`${unchanged} already current \u2014 nothing written`);
19942
+ return `${items.length} script${items.length === 1 ? "" : "s"} (${seg.join("; ")})`;
19943
+ }
17863
19944
  function applyOutcomeSummary(result) {
17864
19945
  const parts = [
17865
19946
  reconcileTally(result.schemaActions, "schema"),
@@ -17868,13 +19949,34 @@ function applyOutcomeSummary(result) {
17868
19949
  // 'reconciled' mean the same "updated to match" as every other kind here.
17869
19950
  reconcileTally(result.issuerActions, "issuer"),
17870
19951
  namespaceTally(result.namespaceActions),
19952
+ // Scripts — reported BEFORE triggers, mirroring the order they are provisioned in, and with
19953
+ // their own wording because "reconciled" would be a lie: a script version is immutable.
19954
+ scriptTally(result.scriptActions),
19955
+ // Trigger rules — 'reconciled' means the same "upsert applied the declared shape" as
19956
+ // schemas/roles (createTrigger's own upsert:true reconcile), so the shared reconcileTally
19957
+ // wording applies unchanged; buildPlan's dry-run preview already discloses each trigger's own
19958
+ // steps individually, this is the apply-time tally counterpart.
19959
+ reconcileTally(result.triggerActions, "trigger"),
17871
19960
  `profile ${result.profileAction}`
17872
19961
  ].filter((p) => p !== null);
17873
19962
  const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
17874
19963
  const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
17875
19964
  ` : "";
19965
+ const pruneNotes = result.triggerPrunes.map(
19966
+ (p) => ` \u26A0 trigger '${p.name}' removed from the blueprint \u2014 de-provisioned (rule + access profile deleted)
19967
+ ` + // Not a warning: 404 on the profile means it was already gone, so the end state is exactly
19968
+ // what a clean prune produces. Said out loud only so the absent second delete isn't read as
19969
+ // a silent skip. Every OTHER profile-delete failure lands in triggerPruneSkipped instead.
19970
+ (p.profileDeleted ? "" : ` (its access profile was already absent \u2014 nothing left to remove)
19971
+ `)
19972
+ ).join("");
19973
+ const pruneSkippedNotes = result.triggerPruneSkipped.map(
19974
+ (s) => s.name ? ` \u26A0 trigger '${s.name}' is no longer declared but could NOT be removed \u2014 ${s.reason}. It is still live.
19975
+ ` : ` \u26A0 triggers could not be reconciled this run \u2014 ${s.reason}. A trigger removed from this blueprint may still be live.
19976
+ `
19977
+ ).join("");
17876
19978
  return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
17877
- ${principalNote}`;
19979
+ ${principalNote}${pruneNotes}${pruneSkippedNotes}`;
17878
19980
  }
17879
19981
  var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
17880
19982
 
@@ -18073,7 +20175,7 @@ ${BOOTSTRAP_HELP}
18073
20175
  process.stderr.write(`
18074
20176
  \u2716 Scope gate refused blueprint '${blueprint.name}':
18075
20177
  `);
18076
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20178
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
18077
20179
  `);
18078
20180
  process.stderr.write(" Minting nothing.\n");
18079
20181
  return 2;
@@ -18082,7 +20184,7 @@ ${BOOTSTRAP_HELP}
18082
20184
  process.stderr.write(`
18083
20185
  \u2716 Capability gate refused blueprint '${blueprint.name}':
18084
20186
  `);
18085
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20187
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
18086
20188
  `);
18087
20189
  process.stderr.write(" Minting nothing.\n");
18088
20190
  return 2;
@@ -18396,71 +20498,6 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
18396
20498
  // src/cli-blueprint.ts
18397
20499
  import { writeFile as writeFile2, access } from "fs/promises";
18398
20500
 
18399
- // src/scope-grammar.ts
18400
- var FORBIDDEN_SCOPE_NAMESPACES2 = /* @__PURE__ */ new Set([
18401
- "user",
18402
- "record",
18403
- "document",
18404
- "entity",
18405
- "self",
18406
- "tenant",
18407
- "context",
18408
- "scope",
18409
- "versions",
18410
- "lookup"
18411
- ]);
18412
- var SCOPE_NAMESPACE_RE2 = /^[a-z][a-z0-9_-]{1,31}$/;
18413
- var SCOPE_NAMESPACE_MIN_LENGTH = 2;
18414
- var SCOPE_NAMESPACE_MAX_LENGTH = 32;
18415
- var SCOPE_VALUE_RE2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
18416
- var SCOPE_VALUE_MIN_LENGTH = 1;
18417
- var SCOPE_VALUE_MAX_LENGTH = 128;
18418
- function valueGrammarError(value) {
18419
- if (!SCOPE_VALUE_RE2.test(value)) {
18420
- return `'${value}' is not a valid scope value \u2014 ${SCOPE_VALUE_MIN_LENGTH}-${SCOPE_VALUE_MAX_LENGTH} characters, starting with a letter or digit and continuing with letters, digits, '_' or '-'. A value may not contain a colon.`;
18421
- }
18422
- return null;
18423
- }
18424
- function namespaceGrammarError(ns) {
18425
- if (FORBIDDEN_SCOPE_NAMESPACES2.has(ns)) {
18426
- return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
18427
- }
18428
- if (!SCOPE_NAMESPACE_RE2.test(ns)) {
18429
- return `'${ns}' is not a valid namespace \u2014 lowercase, ${SCOPE_NAMESPACE_MIN_LENGTH}-${SCOPE_NAMESPACE_MAX_LENGTH} characters, starting with a letter and continuing with letters, digits, '_' or '-'`;
18430
- }
18431
- return null;
18432
- }
18433
- function assertScopeEntries(entries, flag) {
18434
- if (!entries?.length) return;
18435
- for (const raw of entries) {
18436
- const sep = raw.indexOf(":");
18437
- if (sep <= 0 || sep === raw.length - 1) {
18438
- throw new Error(`${flag} must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
18439
- }
18440
- const nsError = namespaceGrammarError(raw.slice(0, sep));
18441
- if (nsError) throw new Error(`${flag} namespace ${nsError}`);
18442
- const valError = valueGrammarError(raw.slice(sep + 1));
18443
- if (valError) throw new Error(`${flag} value ${valError}`);
18444
- }
18445
- }
18446
- function assertIdentityOverrideValues(overrides, flag) {
18447
- if (!overrides) return;
18448
- for (const [key, value] of Object.entries(overrides)) {
18449
- if (!key.startsWith("scope:")) continue;
18450
- const error = valueGrammarError(typeof value === "string" ? value : String(value));
18451
- if (error) throw new Error(`${flag} '${key}' value ${error}`);
18452
- }
18453
- }
18454
- function placeholderScopeNamespace(value) {
18455
- const self2 = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
18456
- if (self2) return self2[1];
18457
- const member = MEMBER_SCOPE_PLACEHOLDER_RE2.exec(value);
18458
- return member ? member[1] : null;
18459
- }
18460
- var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
18461
- var UNDER_SELF_PLACEHOLDER_RE2 = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
18462
- var MEMBER_SCOPE_PLACEHOLDER_RE2 = /^\$\{\{\s*member\.scope\.([a-z][a-z0-9_-]{1,31})(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
18463
-
18464
20501
  // src/lint.ts
18465
20502
  var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
18466
20503
  var BUILT_IN_SORT_BY = /* @__PURE__ */ new Set(["createdAt", "lastUpdated"]);
@@ -18609,7 +20646,7 @@ function lintBlueprint(bp) {
18609
20646
  );
18610
20647
  }
18611
20648
  for (const roleId of Object.keys(bp.roles ?? {})) {
18612
- if (!ROLE_ID_RE.test(roleId)) {
20649
+ if (!ROLE_ID_RE3.test(roleId)) {
18613
20650
  findings.push({
18614
20651
  severity: "error",
18615
20652
  rule: "role-id-format",
@@ -18656,7 +20693,7 @@ function lintBlueprint(bp) {
18656
20693
  }
18657
20694
  return findings;
18658
20695
  }
18659
- var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
20696
+ var ROLE_ID_RE3 = /^[a-z][a-z0-9-]{2,30}$/;
18660
20697
  var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
18661
20698
  var RECOGNIZED_PLACEHOLDER_RES = [
18662
20699
  /^\$\{\{\s*any\s*\}\}$/,
@@ -18922,14 +20959,14 @@ async function resolveAndValidate(arg, verb, supplied) {
18922
20959
  if (err instanceof ScopeGateError) {
18923
20960
  process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
18924
20961
  `);
18925
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20962
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
18926
20963
  `);
18927
20964
  return null;
18928
20965
  }
18929
20966
  if (err instanceof CapabilityGateError) {
18930
20967
  process.stderr.write(` \u2716 Capability gate rejected '${blueprint.name}':
18931
20968
  `);
18932
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20969
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
18933
20970
  `);
18934
20971
  return null;
18935
20972
  }
@@ -19073,137 +21110,44 @@ async function runBlueprintCli(argv, deps = {}) {
19073
21110
  args = parseBlueprintArgs(argv);
19074
21111
  } catch (err) {
19075
21112
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}
19076
-
19077
- ${BLUEPRINT_HELP}
19078
- `);
19079
- return 1;
19080
- }
19081
- if (args.help) {
19082
- process.stdout.write(`${BLUEPRINT_HELP}
19083
- `);
19084
- return 0;
19085
- }
19086
- if (!args.sub) {
19087
- process.stdout.write(`${BLUEPRINT_HELP}
19088
- `);
19089
- return 1;
19090
- }
19091
- switch (args.sub) {
19092
- case "init":
19093
- return runInit(args);
19094
- case "validate":
19095
- return runValidate(args);
19096
- case "plan":
19097
- return runPlan(args);
19098
- case "list":
19099
- return runList();
19100
- default:
19101
- process.stderr.write(
19102
- `Unknown blueprint subcommand: ${args.sub}. Bundled: ${BLUEPRINT_NAMES.join(", ")}
19103
-
19104
- ${BLUEPRINT_HELP}
19105
- `
19106
- );
19107
- return 1;
19108
- }
19109
- }
19110
-
19111
- // src/program.ts
19112
- import { Command, Option } from "commander";
19113
-
19114
- // src/verb-support.ts
19115
- async function withAuth(opts, deps, body) {
19116
- const log = deps.log ?? createLogger2();
19117
- const resolveAuth = deps.resolveAuth ?? resolveAuthContext;
19118
- let auth;
19119
- try {
19120
- auth = await resolveAuth({
19121
- env: opts.env,
19122
- baseUrl: opts.baseUrl,
19123
- token: opts.token,
19124
- context: opts.context,
19125
- provisioning: opts.provisioning,
19126
- tenant: opts.tenant,
19127
- useScopedToken: opts.useScopedToken,
19128
- log
19129
- });
19130
- } catch (err) {
19131
- if (err instanceof MissingCredentialError) {
19132
- process.stderr.write(` \u2716 ${err.message}
19133
- `);
19134
- return 2;
19135
- }
19136
- process.stderr.write(` \u2716 ${formatError(err)}
19137
- `);
19138
- return 1;
19139
- }
19140
- return body(auth, log);
19141
- }
19142
- function requireTenantId(auth) {
19143
- if (!auth.tenantId) {
19144
- throw new Error(
19145
- "Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed."
19146
- );
19147
- }
19148
- return auth.tenantId;
19149
- }
19150
- function normalizePrincipalUserId(principal) {
19151
- return principal.startsWith("usr_") ? principal.slice("usr_".length) : principal;
19152
- }
19153
- function splitCsv(csv) {
19154
- return csv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
19155
- }
19156
- function parseScopeOverrides(scopes) {
19157
- if (!scopes || scopes.length === 0) return void 0;
19158
- const out = {};
19159
- for (const raw of scopes) {
19160
- const sep = raw.indexOf(":");
19161
- if (sep <= 0 || sep === raw.length - 1) {
19162
- throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
19163
- }
19164
- const ns = raw.slice(0, sep);
19165
- const value = raw.slice(sep + 1);
19166
- const valueError = valueGrammarError(value);
19167
- if (valueError) throw new Error(`--scope value ${valueError}`);
19168
- const key = `scope:${ns}`;
19169
- if (Object.prototype.hasOwnProperty.call(out, key)) {
19170
- throw new Error(`--scope sets namespace '${ns}' more than once \u2014 a scope override takes at most one value per namespace.`);
19171
- }
19172
- out[key] = value;
19173
- }
19174
- if (Object.keys(out).length > 2) {
19175
- throw new Error("--scope allows at most two distinct namespaces.");
19176
- }
19177
- return out;
19178
- }
19179
- function parseJsonObjectFlag(flag, raw) {
19180
- let parsed;
19181
- try {
19182
- parsed = JSON.parse(raw);
19183
- } catch {
19184
- throw new Error(`${flag} is not valid JSON: ${raw}`);
21113
+
21114
+ ${BLUEPRINT_HELP}
21115
+ `);
21116
+ return 1;
19185
21117
  }
19186
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
19187
- throw new Error(`${flag} must be a JSON object (e.g. '{"userId":["<id>"]}').`);
21118
+ if (args.help) {
21119
+ process.stdout.write(`${BLUEPRINT_HELP}
21120
+ `);
21121
+ return 0;
19188
21122
  }
19189
- return parsed;
19190
- }
21123
+ if (!args.sub) {
21124
+ process.stdout.write(`${BLUEPRINT_HELP}
21125
+ `);
21126
+ return 1;
21127
+ }
21128
+ switch (args.sub) {
21129
+ case "init":
21130
+ return runInit(args);
21131
+ case "validate":
21132
+ return runValidate(args);
21133
+ case "plan":
21134
+ return runPlan(args);
21135
+ case "list":
21136
+ return runList();
21137
+ default:
21138
+ process.stderr.write(
21139
+ `Unknown blueprint subcommand: ${args.sub}. Bundled: ${BLUEPRINT_NAMES.join(", ")}
19191
21140
 
19192
- // src/paginate.ts
19193
- var PAGE_LIMIT = 100;
19194
- async function drainPages(fetch2) {
19195
- const out = [];
19196
- let startFrom;
19197
- for (; ; ) {
19198
- const page = await fetch2(startFrom);
19199
- if (page.data) out.push(...page.data);
19200
- const next = page.nextCursor;
19201
- if (!next) break;
19202
- startFrom = next;
21141
+ ${BLUEPRINT_HELP}
21142
+ `
21143
+ );
21144
+ return 1;
19203
21145
  }
19204
- return out;
19205
21146
  }
19206
21147
 
21148
+ // src/program.ts
21149
+ import { Command, Option } from "commander";
21150
+
19207
21151
  // src/self-principal.ts
19208
21152
  var PRINCIPAL_PREFIX_USER = "usr_";
19209
21153
  function emailFromJwt(token) {
@@ -20221,7 +22165,14 @@ function normalizeClauses(raw) {
20221
22165
  });
20222
22166
  if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
20223
22167
  }
20224
- out.push({ actions: Array.isArray(actions) ? actions.map(String) : [], dataScope, unrecognizedDims });
22168
+ const rawAssignable = obj.assignable_roles ?? obj.assignableRoles;
22169
+ const assignableRoles = Array.isArray(rawAssignable) && rawAssignable.length > 0 ? rawAssignable.map(String) : void 0;
22170
+ out.push({
22171
+ actions: Array.isArray(actions) ? actions.map(String) : [],
22172
+ dataScope,
22173
+ unrecognizedDims,
22174
+ ...assignableRoles ? { assignableRoles } : {}
22175
+ });
20225
22176
  }
20226
22177
  return out;
20227
22178
  }
@@ -20421,7 +22372,10 @@ async function runAccessExplain(opts, deps = {}) {
20421
22372
  dataScope: renderDataScope(c.dataScope),
20422
22373
  // Present only on a dead clause — an unrecognized dim the platform
20423
22374
  // denies (a retired key or a bad scope:<ns>); the matrix omits it.
20424
- ...c.unrecognizedDims.length > 0 ? { grantsNothing: true, unrecognizedDims: c.unrecognizedDims } : {}
22375
+ ...c.unrecognizedDims.length > 0 ? { grantsNothing: true, unrecognizedDims: c.unrecognizedDims } : {},
22376
+ // Reported only when the clause actually carries the restriction. Absent means
22377
+ // default-open (any role composable), which is a different fact from an empty list.
22378
+ ...c.assignableRoles?.length ? { assignableRoles: c.assignableRoles } : {}
20425
22379
  })),
20426
22380
  matrix: rows,
20427
22381
  // Present only when a clause uses `${{ under.self.* }}`: those rows are UNDECIDED
@@ -20447,6 +22401,12 @@ async function runAccessExplain(opts, deps = {}) {
20447
22401
  if (c.unrecognizedDims.length > 0) {
20448
22402
  process.stdout.write(
20449
22403
  ` \u2716 grants nothing \u2014 unrecognized dimension${c.unrecognizedDims.length > 1 ? "s" : ""}: ${c.unrecognizedDims.join(", ")} (the platform denies a clause keyed on any dim that is not userId or a valid scope:<namespace>)
22404
+ `
22405
+ );
22406
+ }
22407
+ if (c.assignableRoles?.length) {
22408
+ process.stdout.write(
22409
+ ` may compose only these roles into a delegated profile: ${c.assignableRoles.join(", ")}
20450
22410
  `
20451
22411
  );
20452
22412
  }
@@ -20512,6 +22472,14 @@ async function runRoleCreate(opts, deps = {}) {
20512
22472
  return 2;
20513
22473
  }
20514
22474
  }
22475
+ let assignableRoles;
22476
+ try {
22477
+ assignableRoles = parseAssignableRoles(opts.assignableRole);
22478
+ } catch (err) {
22479
+ process.stderr.write(` \u2716 ${formatError(err)}
22480
+ `);
22481
+ return 2;
22482
+ }
20515
22483
  return withAuth(opts, deps, async (auth) => {
20516
22484
  const client = auth.client;
20517
22485
  try {
@@ -20524,7 +22492,15 @@ async function runRoleCreate(opts, deps = {}) {
20524
22492
  // data_scope is included only when supplied — the clause wire shape
20525
22493
  // stays byte-identical for unscoped roles.
20526
22494
  scopes: [
20527
- dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
22495
+ {
22496
+ allowed_actions: actions,
22497
+ // Each included only when supplied — the clause wire shape stays byte-identical for an
22498
+ // unscoped, unrestricted role. `assignable_roles` in particular must be OMITTED rather
22499
+ // than sent empty: the platform reads absent as "no roleId restriction" and rejects
22500
+ // `[]` outright, so the two are not interchangeable.
22501
+ ...dataScope ? { data_scope: dataScope } : {},
22502
+ ...assignableRoles ? { assignable_roles: assignableRoles } : {}
22503
+ }
20528
22504
  ],
20529
22505
  // Included only when supplied — omitting it (rather than sending an
20530
22506
  // empty object) keeps "no assume grant" and "explicitly empty grant"
@@ -20783,6 +22759,36 @@ function parseSelfSignupPolicies(raw) {
20783
22759
  return { signup_type: entry.slice(0, sep), role_id: entry.slice(sep + 1) };
20784
22760
  });
20785
22761
  }
22762
+ var MAX_CAPTURED_CLAIMS2 = 20;
22763
+ var MAX_CAPTURED_CLAIM_NAME_LENGTH2 = 64;
22764
+ function parseCapturedClaims(raw) {
22765
+ if (!raw || raw.length === 0) return void 0;
22766
+ if (raw.length > MAX_CAPTURED_CLAIMS2) {
22767
+ throw new Error(
22768
+ `--captured-claim accepts at most ${MAX_CAPTURED_CLAIMS2} entries; got ${raw.length}.`
22769
+ );
22770
+ }
22771
+ const seen = /* @__PURE__ */ new Set();
22772
+ for (const name of raw) {
22773
+ if (name.trim().length === 0) {
22774
+ throw new Error("--captured-claim must be a non-blank OIDC claim name (e.g. 'phone_number').");
22775
+ }
22776
+ if (name.length > MAX_CAPTURED_CLAIM_NAME_LENGTH2) {
22777
+ throw new Error(
22778
+ `--captured-claim '${name}' is ${name.length} characters, above the ${MAX_CAPTURED_CLAIM_NAME_LENGTH2}-character limit.`
22779
+ );
22780
+ }
22781
+ if (seen.has(name)) {
22782
+ throw new Error(`--captured-claim repeats '${name}' \u2014 each claim name may be given only once.`);
22783
+ }
22784
+ seen.add(name);
22785
+ }
22786
+ return [...raw];
22787
+ }
22788
+ function renderCapturedClaims(claims) {
22789
+ if (!claims || claims.length === 0) return "\u2014";
22790
+ return claims.join(", ");
22791
+ }
20786
22792
  function renderSelfSignup(policies) {
20787
22793
  if (!policies || policies.length === 0) return "\u2014";
20788
22794
  return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
@@ -20799,7 +22805,8 @@ function renderIssuer(i) {
20799
22805
  ["userinfoUri", i.userinfoUri],
20800
22806
  ["status", i.status],
20801
22807
  ["createdAt", i.createdAt],
20802
- ["selfSignup", renderSelfSignup(i.selfSignupPolicies)]
22808
+ ["selfSignup", renderSelfSignup(i.selfSignupPolicies)],
22809
+ ["capturedClaims", renderCapturedClaims(i.capturedClaims)]
20803
22810
  ]);
20804
22811
  }
20805
22812
  async function runIssuersList(opts, deps = {}) {
@@ -20820,7 +22827,8 @@ async function runIssuersList(opts, deps = {}) {
20820
22827
  { header: "issuer", get: (i) => i.issuer },
20821
22828
  { header: "audience", get: (i) => i.audience },
20822
22829
  { header: "status", get: (i) => i.status },
20823
- { header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) }
22830
+ { header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) },
22831
+ { header: "captured claims", get: (i) => renderCapturedClaims(i.capturedClaims) }
20824
22832
  ]) + "\n"
20825
22833
  );
20826
22834
  return 0;
@@ -20852,8 +22860,10 @@ ${renderIssuer(issuer)}
20852
22860
  }
20853
22861
  async function runIssuersRegister(opts, deps = {}) {
20854
22862
  let selfSignupPolicies;
22863
+ let capturedClaims;
20855
22864
  try {
20856
22865
  selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
22866
+ capturedClaims = parseCapturedClaims(opts.capturedClaim);
20857
22867
  } catch (err) {
20858
22868
  process.stderr.write(` \u2716 ${formatError(err)}
20859
22869
  `);
@@ -20871,7 +22881,8 @@ async function runIssuersRegister(opts, deps = {}) {
20871
22881
  subClaim: opts.subClaim,
20872
22882
  emailClaim: opts.emailClaim,
20873
22883
  userinfoUri: opts.userinfoUri,
20874
- selfSignupPolicies
22884
+ selfSignupPolicies,
22885
+ capturedClaims
20875
22886
  });
20876
22887
  if (opts.json) {
20877
22888
  process.stdout.write(`${formatJson(issuer)}
@@ -20892,8 +22903,10 @@ ${renderIssuer(issuer)}
20892
22903
  }
20893
22904
  async function runIssuersUpdate(opts, deps = {}) {
20894
22905
  let selfSignupPolicies;
22906
+ let capturedClaims;
20895
22907
  try {
20896
22908
  selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
22909
+ capturedClaims = parseCapturedClaims(opts.capturedClaim);
20897
22910
  } catch (err) {
20898
22911
  process.stderr.write(` \u2716 ${formatError(err)}
20899
22912
  `);
@@ -20905,6 +22918,12 @@ async function runIssuersUpdate(opts, deps = {}) {
20905
22918
  );
20906
22919
  return 2;
20907
22920
  }
22921
+ if (opts.clearCapturedClaims && capturedClaims && capturedClaims.length > 0) {
22922
+ process.stderr.write(
22923
+ " \u2716 --clear-captured-claims and --captured-claim are mutually exclusive.\n"
22924
+ );
22925
+ return 2;
22926
+ }
20908
22927
  return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20909
22928
  const client = auth.client;
20910
22929
  try {
@@ -20914,7 +22933,8 @@ async function runIssuersUpdate(opts, deps = {}) {
20914
22933
  emailClaim: opts.emailClaim,
20915
22934
  userinfoUri: opts.userinfoUri,
20916
22935
  status: opts.status,
20917
- selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies
22936
+ selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies,
22937
+ capturedClaims: opts.clearCapturedClaims ? [] : capturedClaims
20918
22938
  });
20919
22939
  if (opts.json) {
20920
22940
  process.stdout.write(`${formatJson(issuer)}
@@ -20973,41 +22993,264 @@ async function runIssuersDelete(opts, deps = {}) {
20973
22993
  try {
20974
22994
  await client.auth.deleteIssuer({ issuerId });
20975
22995
  if (opts.json) {
20976
- process.stdout.write(`${formatJson({ issuerId, deleted: true })}
22996
+ process.stdout.write(`${formatJson({ issuerId, deleted: true })}
22997
+ `);
22998
+ return 0;
22999
+ }
23000
+ process.stdout.write(`
23001
+ \u2714 issuer '${issuerId}' deregistered from '${context}'
23002
+ `);
23003
+ return 0;
23004
+ } catch (err) {
23005
+ process.stderr.write(formatCliError(err, { action: "issuers delete failed" }));
23006
+ return 1;
23007
+ }
23008
+ });
23009
+ }
23010
+
23011
+ // src/commands/namespace.ts
23012
+ function renderNamespace(n) {
23013
+ return formatKeyValues([
23014
+ ["namespace", n.namespace],
23015
+ ["contextId", n.contextId ?? "(tenant-wide)"],
23016
+ ["entityBacked", n.entityBacked === void 0 ? void 0 : String(n.entityBacked)],
23017
+ ["reserved", n.reserved === void 0 ? void 0 : String(n.reserved)],
23018
+ ["specificityRank", n.specificityRank === void 0 ? void 0 : String(n.specificityRank)],
23019
+ ["defaultSchemaId", n.defaultSchemaId],
23020
+ ["createdAt", n.createdAt]
23021
+ ]);
23022
+ }
23023
+ async function runNamespaceList(opts, deps = {}) {
23024
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
23025
+ const client = auth.client;
23026
+ try {
23027
+ const rows = await drainPages(
23028
+ (startFrom) => client.identity.listNamespaces({
23029
+ limit: PAGE_LIMIT,
23030
+ ...opts.context ? { contextId: opts.context } : {},
23031
+ ...startFrom ? { startFrom } : {}
23032
+ })
23033
+ );
23034
+ if (opts.json) {
23035
+ process.stdout.write(`${formatJson(rows)}
23036
+ `);
23037
+ return 0;
23038
+ }
23039
+ process.stdout.write(
23040
+ "\n" + formatTable(rows, [
23041
+ { header: "namespace", get: (n) => n.namespace },
23042
+ { header: "contextId", get: (n) => n.contextId ?? "(tenant-wide)" },
23043
+ {
23044
+ header: "entityBacked",
23045
+ get: (n) => n.entityBacked === void 0 ? void 0 : String(n.entityBacked)
23046
+ },
23047
+ {
23048
+ header: "specificityRank",
23049
+ get: (n) => n.specificityRank === void 0 ? void 0 : String(n.specificityRank)
23050
+ }
23051
+ ]) + "\n"
23052
+ );
23053
+ return 0;
23054
+ } catch (err) {
23055
+ process.stderr.write(formatCliError(err, { action: "namespace list failed" }));
23056
+ return 1;
23057
+ }
23058
+ });
23059
+ }
23060
+ async function runNamespaceGet(opts, deps = {}) {
23061
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
23062
+ const client = auth.client;
23063
+ try {
23064
+ const ns = await client.identity.getNamespace({
23065
+ namespace: opts.namespace,
23066
+ ...opts.context ? { contextId: opts.context } : {}
23067
+ });
23068
+ if (opts.json) {
23069
+ process.stdout.write(`${formatJson(ns)}
23070
+ `);
23071
+ return 0;
23072
+ }
23073
+ process.stdout.write(`
23074
+ ${renderNamespace(ns)}
23075
+ `);
23076
+ return 0;
23077
+ } catch (err) {
23078
+ process.stderr.write(formatCliError(err, { action: "namespace get failed" }));
23079
+ return 1;
23080
+ }
23081
+ });
23082
+ }
23083
+ async function runNamespaceRegister(opts, deps = {}) {
23084
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
23085
+ const client = auth.client;
23086
+ try {
23087
+ const ns = await client.identity.registerNamespace({
23088
+ contextId: opts.context,
23089
+ body: {
23090
+ namespace: opts.namespace,
23091
+ specificityRank: opts.specificityRank,
23092
+ entityBacked: opts.entityBacked,
23093
+ defaultSchemaId: opts.defaultSchemaId
23094
+ }
23095
+ });
23096
+ if (opts.json) {
23097
+ process.stdout.write(`${formatJson(ns)}
23098
+ `);
23099
+ return 0;
23100
+ }
23101
+ process.stdout.write(`
23102
+ \u2714 namespace '${opts.namespace}' registered
23103
+ ${renderNamespace(ns)}
23104
+ `);
23105
+ return 0;
23106
+ } catch (err) {
23107
+ process.stderr.write(formatCliError(err, { action: "namespace register failed" }));
23108
+ return 1;
23109
+ }
23110
+ });
23111
+ }
23112
+
23113
+ // src/commands/scripts.ts
23114
+ import { readFile as readFile3 } from "fs/promises";
23115
+ import * as readline5 from "readline/promises";
23116
+ var SOURCE_MAX_BYTES = 3e5;
23117
+ var INPUT_CONTRACT_MAX_BYTES = 5e4;
23118
+ var utf8Bytes2 = (s) => new TextEncoder().encode(s).length;
23119
+ var SCRIPT_NAME_RE2 = /^[A-Za-z0-9_-]{1,64}$/;
23120
+ function renderScript(s, opts = {}) {
23121
+ return formatKeyValues([
23122
+ ["id", s.id],
23123
+ ["name", s.name],
23124
+ ["scriptVersion", s.scriptVersion === void 0 ? void 0 : String(s.scriptVersion)],
23125
+ ["declaredInputContract", s.declaredInputContract],
23126
+ ["createdAt", s.createdAt],
23127
+ // Full source last, and only where it was asked for — it can be 300 KB.
23128
+ ...opts.source && s.source !== void 0 ? [["source", s.source]] : []
23129
+ ]);
23130
+ }
23131
+ async function runScriptsPush(opts, deps = {}) {
23132
+ if (!SCRIPT_NAME_RE2.test(opts.name)) {
23133
+ process.stderr.write(
23134
+ ` \u2716 '${opts.name}' is not a valid script name \u2014 use 1-64 characters of letters, digits, underscores ('_') and hyphens ('-').
23135
+ `
23136
+ );
23137
+ return 2;
23138
+ }
23139
+ if (opts.inputContract !== void 0 && opts.inputContractFile !== void 0) {
23140
+ process.stderr.write(" \u2716 --input-contract and --input-contract-file are mutually exclusive.\n");
23141
+ return 2;
23142
+ }
23143
+ let source;
23144
+ try {
23145
+ source = await readFile3(opts.sourceFile, "utf8");
23146
+ } catch (err) {
23147
+ process.stderr.write(` \u2716 could not read --source-file '${opts.sourceFile}': ${errText(err)}
23148
+ `);
23149
+ return 2;
23150
+ }
23151
+ if (source.trim().length === 0) {
23152
+ process.stderr.write(` \u2716 --source-file '${opts.sourceFile}' is empty \u2014 'source' must not be blank.
23153
+ `);
23154
+ return 2;
23155
+ }
23156
+ if (opts.inputContract !== void 0 && opts.inputContract.trim().length === 0) {
23157
+ process.stderr.write(
23158
+ ` \u2716 --input-contract is blank. Omit the flag to store no input contract at all \u2014 an empty one is not a contract.
23159
+ `
23160
+ );
23161
+ return 2;
23162
+ }
23163
+ let declaredInputContract = opts.inputContract;
23164
+ if (opts.inputContractFile !== void 0) {
23165
+ try {
23166
+ declaredInputContract = await readFile3(opts.inputContractFile, "utf8");
23167
+ } catch (err) {
23168
+ process.stderr.write(
23169
+ ` \u2716 could not read --input-contract-file '${opts.inputContractFile}': ${errText(err)}
23170
+ `
23171
+ );
23172
+ return 2;
23173
+ }
23174
+ if (declaredInputContract.trim().length === 0) {
23175
+ process.stderr.write(
23176
+ ` \u2716 --input-contract-file '${opts.inputContractFile}' is blank. Omit the flag to store no input contract at all \u2014 an empty one is not a contract.
23177
+ `
23178
+ );
23179
+ return 2;
23180
+ }
23181
+ }
23182
+ const sourceBytes = utf8Bytes2(source);
23183
+ if (sourceBytes > SOURCE_MAX_BYTES) {
23184
+ process.stderr.write(
23185
+ ` \u2716 source is ${sourceBytes.toLocaleString("en-US")} UTF-8 bytes, over the ${SOURCE_MAX_BYTES.toLocaleString("en-US")}-byte limit.
23186
+ `
23187
+ );
23188
+ return 2;
23189
+ }
23190
+ if (declaredInputContract !== void 0 && utf8Bytes2(declaredInputContract) > INPUT_CONTRACT_MAX_BYTES) {
23191
+ process.stderr.write(
23192
+ ` \u2716 input contract is ${utf8Bytes2(declaredInputContract).toLocaleString("en-US")} UTF-8 bytes, over the ${INPUT_CONTRACT_MAX_BYTES.toLocaleString("en-US")}-byte limit.
23193
+ `
23194
+ );
23195
+ return 2;
23196
+ }
23197
+ return withAuth(opts, deps, async (auth) => {
23198
+ const client = auth.client;
23199
+ try {
23200
+ if (opts.ifChanged) {
23201
+ const latest = await readLatestScriptVersion(
23202
+ client,
23203
+ opts.name
23204
+ );
23205
+ if (latest !== void 0 && scriptMatches(latest, { source, declaredInputContract })) {
23206
+ if (opts.json) {
23207
+ process.stdout.write(`${formatJson({ ...latest, pushed: false })}
23208
+ `);
23209
+ return 0;
23210
+ }
23211
+ process.stdout.write(
23212
+ `
23213
+ = script '${opts.name}' already matches at version ${latest.scriptVersion} \u2014 nothing pushed.
23214
+ `
23215
+ );
23216
+ return 0;
23217
+ }
23218
+ }
23219
+ const pushed = await client.scripts.createScript({
23220
+ name: opts.name,
23221
+ source,
23222
+ declaredInputContract
23223
+ });
23224
+ if (opts.json) {
23225
+ process.stdout.write(`${formatJson({ ...pushed, pushed: true })}
20977
23226
  `);
20978
23227
  return 0;
20979
23228
  }
20980
- process.stdout.write(`
20981
- \u2714 issuer '${issuerId}' deregistered from '${context}'
20982
- `);
23229
+ process.stdout.write(
23230
+ `
23231
+ \u2714 pushed script '${opts.name}' version ${pushed.scriptVersion} (${sourceBytes.toLocaleString("en-US")} bytes) to '${opts.context}'
23232
+ ${renderScript(pushed)}
23233
+ A trigger rule reaches this with scriptRef { name: '${opts.name}', version: 'latest' }.
23234
+ Pinning version '${pushed.scriptVersion}' instead keeps that rule on exactly this one \u2014
23235
+ but only for a script pushed this way. A blueprint that SHIPS a script requires 'latest'
23236
+ for it, since the blueprint cannot know which number its source will land on.
23237
+ `
23238
+ );
20983
23239
  return 0;
20984
23240
  } catch (err) {
20985
- process.stderr.write(formatCliError(err, { action: "issuers delete failed" }));
23241
+ process.stderr.write(formatCliError(err, { action: "scripts push failed" }));
20986
23242
  return 1;
20987
23243
  }
20988
23244
  });
20989
23245
  }
20990
-
20991
- // src/commands/namespace.ts
20992
- function renderNamespace(n) {
20993
- return formatKeyValues([
20994
- ["namespace", n.namespace],
20995
- ["contextId", n.contextId ?? "(tenant-wide)"],
20996
- ["entityBacked", n.entityBacked === void 0 ? void 0 : String(n.entityBacked)],
20997
- ["reserved", n.reserved === void 0 ? void 0 : String(n.reserved)],
20998
- ["specificityRank", n.specificityRank === void 0 ? void 0 : String(n.specificityRank)],
20999
- ["defaultSchemaId", n.defaultSchemaId],
21000
- ["createdAt", n.createdAt]
21001
- ]);
21002
- }
21003
- async function runNamespaceList(opts, deps = {}) {
21004
- return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
23246
+ async function runScriptsList(opts, deps = {}) {
23247
+ return withAuth(opts, deps, async (auth) => {
21005
23248
  const client = auth.client;
21006
23249
  try {
21007
23250
  const rows = await drainPages(
21008
- (startFrom) => client.identity.listNamespaces({
23251
+ (startFrom) => client.scripts.listScripts({
21009
23252
  limit: PAGE_LIMIT,
21010
- ...opts.context ? { contextId: opts.context } : {},
23253
+ ...opts.name ? { name: opts.name } : {},
21011
23254
  ...startFrom ? { startFrom } : {}
21012
23255
  })
21013
23256
  );
@@ -21016,79 +23259,113 @@ async function runNamespaceList(opts, deps = {}) {
21016
23259
  `);
21017
23260
  return 0;
21018
23261
  }
23262
+ if (rows.length === 0) {
23263
+ process.stdout.write(
23264
+ opts.name ? `
23265
+ No versions of script '${opts.name}' in '${opts.context}'.
23266
+ ` : `
23267
+ No scripts in '${opts.context}'.
23268
+ `
23269
+ );
23270
+ return 0;
23271
+ }
21019
23272
  process.stdout.write(
21020
23273
  "\n" + formatTable(rows, [
21021
- { header: "namespace", get: (n) => n.namespace },
21022
- { header: "contextId", get: (n) => n.contextId ?? "(tenant-wide)" },
21023
- {
21024
- header: "entityBacked",
21025
- get: (n) => n.entityBacked === void 0 ? void 0 : String(n.entityBacked)
21026
- },
21027
- {
21028
- header: "specificityRank",
21029
- get: (n) => n.specificityRank === void 0 ? void 0 : String(n.specificityRank)
21030
- }
23274
+ { header: "name", get: (s) => s.name },
23275
+ { header: "version", get: (s) => s.scriptVersion === void 0 ? void 0 : String(s.scriptVersion) },
23276
+ { header: "id", get: (s) => s.id },
23277
+ { header: "bytes", get: (s) => s.source === void 0 ? void 0 : String(utf8Bytes2(s.source)) },
23278
+ { header: "createdAt", get: (s) => s.createdAt }
21031
23279
  ]) + "\n"
21032
23280
  );
21033
23281
  return 0;
21034
23282
  } catch (err) {
21035
- process.stderr.write(formatCliError(err, { action: "namespace list failed" }));
23283
+ process.stderr.write(formatCliError(err, { action: "scripts list failed" }));
21036
23284
  return 1;
21037
23285
  }
21038
23286
  });
21039
23287
  }
21040
- async function runNamespaceGet(opts, deps = {}) {
21041
- return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
23288
+ async function runScriptsGet(opts, deps = {}) {
23289
+ return withAuth(opts, deps, async (auth) => {
21042
23290
  const client = auth.client;
21043
23291
  try {
21044
- const ns = await client.identity.getNamespace({
21045
- namespace: opts.namespace,
21046
- ...opts.context ? { contextId: opts.context } : {}
21047
- });
23292
+ const script = await client.scripts.getScript({ id: opts.id });
21048
23293
  if (opts.json) {
21049
- process.stdout.write(`${formatJson(ns)}
23294
+ process.stdout.write(`${formatJson(script)}
21050
23295
  `);
21051
23296
  return 0;
21052
23297
  }
21053
23298
  process.stdout.write(`
21054
- ${renderNamespace(ns)}
23299
+ ${renderScript(script, { source: true })}
21055
23300
  `);
21056
23301
  return 0;
21057
23302
  } catch (err) {
21058
- process.stderr.write(formatCliError(err, { action: "namespace get failed" }));
23303
+ process.stderr.write(formatCliError(err, { action: "scripts get failed" }));
21059
23304
  return 1;
21060
23305
  }
21061
23306
  });
21062
23307
  }
21063
- async function runNamespaceRegister(opts, deps = {}) {
21064
- return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
23308
+ async function runScriptsDelete(opts, deps = {}) {
23309
+ process.stderr.write(
23310
+ `
23311
+ \u26A0 Deleting script version '${opts.id}' is permanent \u2014 the stored source is the only copy.
23312
+ Other versions of the same name are unaffected.
23313
+ Nothing checks whether a trigger rule still references this version. A rule pinned to it
23314
+ will fail on every firing; a rule on 'latest' will silently fall back to the previous
23315
+ version and keep running older code. This CLI cannot list trigger rules, so confirm
23316
+ against the API or the portal before removing a version a rule may still use.
23317
+ And if this is the name's LATEST version, its identity is REUSED: the next push of that
23318
+ name recomputes the same version number and the same id, so anything holding a pinned
23319
+ '${opts.id}' or a name@version reference will silently resolve to different source.
23320
+
23321
+ `
23322
+ );
23323
+ if (!opts.force) {
23324
+ const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
23325
+ if (!isTty) {
23326
+ process.stderr.write(
23327
+ ` \u2716 scripts delete is irreversible and needs an interactive confirmation.
23328
+ Re-run with --force to delete '${opts.id}' without a prompt.
23329
+ `
23330
+ );
23331
+ return 2;
23332
+ }
23333
+ const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
23334
+ const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
23335
+ return { prompt: (q) => rl.question(q), close: () => rl.close() };
23336
+ })();
23337
+ try {
23338
+ const typed = await promptCtx.prompt(` Type the version id ('${opts.id}') to confirm: `);
23339
+ if (typed.trim() !== opts.id) {
23340
+ process.stderr.write(` \u2716 Confirmation did not match \u2014 nothing deleted.
23341
+ `);
23342
+ return 1;
23343
+ }
23344
+ } finally {
23345
+ promptCtx.close();
23346
+ }
23347
+ }
23348
+ return withAuth(opts, deps, async (auth) => {
21065
23349
  const client = auth.client;
21066
23350
  try {
21067
- const ns = await client.identity.registerNamespace({
21068
- contextId: opts.context,
21069
- body: {
21070
- namespace: opts.namespace,
21071
- specificityRank: opts.specificityRank,
21072
- entityBacked: opts.entityBacked,
21073
- defaultSchemaId: opts.defaultSchemaId
21074
- }
21075
- });
23351
+ await client.scripts.deleteScript({ id: opts.id });
21076
23352
  if (opts.json) {
21077
- process.stdout.write(`${formatJson(ns)}
23353
+ process.stdout.write(`${formatJson({ id: opts.id, deleted: true })}
21078
23354
  `);
21079
23355
  return 0;
21080
23356
  }
21081
- process.stdout.write(`
21082
- \u2714 namespace '${opts.namespace}' registered
21083
- ${renderNamespace(ns)}
23357
+ process.stdout.write(` \u2714 Deleted script version '${opts.id}' from '${opts.context}'.
21084
23358
  `);
21085
23359
  return 0;
21086
23360
  } catch (err) {
21087
- process.stderr.write(formatCliError(err, { action: "namespace register failed" }));
23361
+ process.stderr.write(formatCliError(err, { action: "scripts delete failed" }));
21088
23362
  return 1;
21089
23363
  }
21090
23364
  });
21091
23365
  }
23366
+ function errText(err) {
23367
+ return err instanceof Error ? err.message : String(err);
23368
+ }
21092
23369
 
21093
23370
  // src/commands/assume.ts
21094
23371
  function parseAssumeScopes(raw) {
@@ -21111,11 +23388,20 @@ function parseAssumeScopes(raw) {
21111
23388
  return out;
21112
23389
  }
21113
23390
  function renderAssumeToken(t) {
21114
- return formatKeyValues([
23391
+ const identity = t.resolvedScope?.identity;
23392
+ const allowed = t.resolvedScope?.allowedActions;
23393
+ const rows = [
21115
23394
  ["access_token", t.access_token],
21116
23395
  ["token_type", t.token_type],
21117
23396
  ["expires_in", t.expires_in === void 0 ? void 0 : `${t.expires_in}s`]
21118
- ]);
23397
+ ];
23398
+ if (identity && Object.keys(identity).length > 0) {
23399
+ rows.push(["identity", Object.entries(identity).map(([k, v]) => `${k}=${v}`).join(", ")]);
23400
+ }
23401
+ if (allowed?.length) {
23402
+ rows.push(["allowedActions", allowed.join(", ")]);
23403
+ }
23404
+ return formatKeyValues(rows);
21119
23405
  }
21120
23406
  async function runAssume(opts, deps = {}) {
21121
23407
  if (!opts.scope || opts.scope.length === 0) {
@@ -21300,12 +23586,14 @@ import { randomUUID as randomUUID2 } from "crypto";
21300
23586
 
21301
23587
  // src/harness.ts
21302
23588
  var errMsg = (e) => e instanceof Error ? e.message : String(e);
21303
- var isNotFound = (e) => typeof e === "object" && e !== null && e.statusCode === 404;
23589
+ var isNotFound2 = (e) => typeof e === "object" && e !== null && e.statusCode === 404;
21304
23590
  var SNAPSHOT_PAGE_SIZE = 100;
21305
23591
  async function captureSnapshot(client, blueprint) {
21306
23592
  const existingSchemaTypes = /* @__PURE__ */ new Set();
21307
23593
  const existingProfilePrincipals = /* @__PURE__ */ new Set();
21308
23594
  const existingRoleIds = /* @__PURE__ */ new Set();
23595
+ const existingTriggerNames = /* @__PURE__ */ new Set();
23596
+ let triggerSnapshotFailed = false;
21309
23597
  let failed = false;
21310
23598
  try {
21311
23599
  let cursor;
@@ -21317,7 +23605,7 @@ async function captureSnapshot(client, blueprint) {
21317
23605
  cursor = next;
21318
23606
  }
21319
23607
  } catch (e) {
21320
- if (!isNotFound(e)) failed = true;
23608
+ if (!isNotFound2(e)) failed = true;
21321
23609
  }
21322
23610
  try {
21323
23611
  let startFrom;
@@ -21333,7 +23621,7 @@ async function captureSnapshot(client, blueprint) {
21333
23621
  startFrom = next;
21334
23622
  }
21335
23623
  } catch (e) {
21336
- if (!isNotFound(e)) failed = true;
23624
+ if (!isNotFound2(e)) failed = true;
21337
23625
  }
21338
23626
  try {
21339
23627
  let startFrom;
@@ -21349,9 +23637,23 @@ async function captureSnapshot(client, blueprint) {
21349
23637
  startFrom = next;
21350
23638
  }
21351
23639
  } catch (e) {
21352
- if (!isNotFound(e)) failed = true;
23640
+ if (!isNotFound2(e)) failed = true;
23641
+ }
23642
+ if (Object.keys(blueprint.triggers ?? {}).length > 0) {
23643
+ try {
23644
+ let startFrom;
23645
+ for (; ; ) {
23646
+ const page = await client.triggers.listTriggers({ limit: SNAPSHOT_PAGE_SIZE, startFrom });
23647
+ for (const t of page.data ?? []) if (t.name) existingTriggerNames.add(t.name);
23648
+ const next = page.nextCursor;
23649
+ if (!next) break;
23650
+ startFrom = next;
23651
+ }
23652
+ } catch (e) {
23653
+ if (!isNotFound2(e)) triggerSnapshotFailed = true;
23654
+ }
21353
23655
  }
21354
- return { existingSchemaTypes, existingProfilePrincipals, existingRoleIds, failed };
23656
+ return { existingSchemaTypes, existingProfilePrincipals, existingRoleIds, existingTriggerNames, triggerSnapshotFailed, failed };
21355
23657
  }
21356
23658
  async function captureIssuerSnapshot(bootstrapClient) {
21357
23659
  const existingIssuerIds = /* @__PURE__ */ new Set();
@@ -21547,6 +23849,97 @@ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapsh
21547
23849
  steps.push({ step: `delete role ${roleId}`, ok: false, error: errMsg(e) });
21548
23850
  }
21549
23851
  }
23852
+ const declaredTriggerNames = Object.keys(blueprint.triggers ?? {});
23853
+ let allDeclaredTriggersDeleted = true;
23854
+ if (declaredTriggerNames.length > 0) {
23855
+ let live = [];
23856
+ let listFailed = false;
23857
+ try {
23858
+ let startFrom;
23859
+ for (; ; ) {
23860
+ const page = await client.triggers.listTriggers({ limit: SNAPSHOT_PAGE_SIZE, startFrom });
23861
+ live = live.concat(page.data ?? []);
23862
+ const next = page.nextCursor;
23863
+ if (!next) break;
23864
+ startFrom = next;
23865
+ }
23866
+ } catch (e) {
23867
+ listFailed = true;
23868
+ steps.push({ step: "delete triggers", ok: false, error: `could not list triggers: ${errMsg(e)}` });
23869
+ allDeclaredTriggersDeleted = false;
23870
+ }
23871
+ if (!listFailed) {
23872
+ for (const name of declaredTriggerNames) {
23873
+ if (snapshot.failed || snapshot.triggerSnapshotFailed) {
23874
+ steps.push({ step: `delete trigger ${name}`, ok: true, skipped: true, error: "skipped \u2014 pre-apply snapshot failed; cannot prove ownership" });
23875
+ allDeclaredTriggersDeleted = false;
23876
+ continue;
23877
+ }
23878
+ if (snapshot.existingTriggerNames.has(name)) {
23879
+ steps.push({ step: `delete trigger ${name}`, ok: true, skipped: true, error: "skipped \u2014 pre-existed this run" });
23880
+ allDeclaredTriggersDeleted = false;
23881
+ continue;
23882
+ }
23883
+ const row = live.find((t) => t.name === name);
23884
+ if (!row?.id) {
23885
+ steps.push({ step: `delete trigger ${name}`, ok: true, skipped: true, error: "skipped \u2014 not found (apply may not have reached it)" });
23886
+ continue;
23887
+ }
23888
+ if (row.provisionedBy !== blueprint.name) {
23889
+ steps.push({
23890
+ step: `delete trigger ${name}`,
23891
+ ok: true,
23892
+ skipped: true,
23893
+ error: `skipped \u2014 provisionedBy is ${row.provisionedBy ? `'${row.provisionedBy}'` : "unset"}, not this blueprint`
23894
+ });
23895
+ allDeclaredTriggersDeleted = false;
23896
+ continue;
23897
+ }
23898
+ try {
23899
+ await client.triggers.deleteTrigger({ id: row.id });
23900
+ steps.push({ step: `delete trigger ${name}`, ok: true });
23901
+ } catch (e) {
23902
+ steps.push({ step: `delete trigger ${name}`, ok: false, error: errMsg(e) });
23903
+ allDeclaredTriggersDeleted = false;
23904
+ continue;
23905
+ }
23906
+ if (row.principalId) {
23907
+ try {
23908
+ await client.auth.deleteAccessProfile({ contextId: blueprint.contextId, principalId: row.principalId });
23909
+ steps.push({ step: `delete trigger ${name} profile`, ok: true });
23910
+ } catch (e) {
23911
+ steps.push({ step: `delete trigger ${name} profile`, ok: false, error: errMsg(e) });
23912
+ }
23913
+ }
23914
+ }
23915
+ }
23916
+ }
23917
+ for (const script of result.scriptActions) {
23918
+ if (!allDeclaredTriggersDeleted) {
23919
+ steps.push({
23920
+ step: `delete script ${script.name} v${script.scriptVersion}`,
23921
+ ok: true,
23922
+ skipped: true,
23923
+ error: "skipped \u2014 a trigger rule this blueprint declares was not removed, so deleting the script it may reference could break a live rule silently"
23924
+ });
23925
+ continue;
23926
+ }
23927
+ if (script.action === "unchanged") {
23928
+ steps.push({
23929
+ step: `delete script ${script.name} v${script.scriptVersion}`,
23930
+ ok: true,
23931
+ skipped: true,
23932
+ error: "skipped \u2014 this run pushed no version (the stored source already matched)"
23933
+ });
23934
+ continue;
23935
+ }
23936
+ try {
23937
+ await client.scripts.deleteScript({ id: script.id });
23938
+ steps.push({ step: `delete script ${script.name} v${script.scriptVersion}`, ok: true });
23939
+ } catch (e) {
23940
+ steps.push({ step: `delete script ${script.name} v${script.scriptVersion}`, ok: false, error: errMsg(e) });
23941
+ }
23942
+ }
21550
23943
  const toDelete = [];
21551
23944
  for (const [typeName, schemaId] of Object.entries(result.schemaIds)) {
21552
23945
  if (snapshot.failed) {
@@ -21633,7 +24026,9 @@ async function runBlueprintHarness(client, input, deps) {
21633
24026
  existingSchemaTypes: /* @__PURE__ */ new Set(),
21634
24027
  existingProfilePrincipals: /* @__PURE__ */ new Set(),
21635
24028
  existingRoleIds: /* @__PURE__ */ new Set(),
21636
- failed: !isNotFound(e)
24029
+ existingTriggerNames: /* @__PURE__ */ new Set(),
24030
+ triggerSnapshotFailed: !isNotFound2(e),
24031
+ failed: !isNotFound2(e)
21637
24032
  };
21638
24033
  }
21639
24034
  } else {
@@ -21646,7 +24041,7 @@ async function runBlueprintHarness(client, input, deps) {
21646
24041
  try {
21647
24042
  issuerSnapshot = await captureIssuerSnapshot(await remintBootstrapForContext(blueprint.contextId));
21648
24043
  } catch (e) {
21649
- issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound(e) };
24044
+ issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound2(e) };
21650
24045
  }
21651
24046
  } else {
21652
24047
  issuerSnapshot = await captureIssuerSnapshot(client);
@@ -21669,7 +24064,17 @@ async function runBlueprintHarness(client, input, deps) {
21669
24064
  log,
21670
24065
  provisioned,
21671
24066
  remintForContext,
21672
- remintBootstrapForContext
24067
+ remintBootstrapForContext,
24068
+ // CREATED-ONLY, and this is the seam that keeps it true for triggers. The loader's step 5d
24069
+ // reconciles triggers by ABSENCE — it deletes rules this blueprint provisioned and no longer
24070
+ // declares. That is right for a real bootstrap run, which owns its context; it is wrong here.
24071
+ // This harness may run against a shared tenant, and its whole safety contract is that a run
24072
+ // touches only what it CREATED: `captureSnapshot` protects pre-existing schemas, profiles,
24073
+ // roles and issuers, and `runTeardown` skips them. There is no trigger leg in either, so a
24074
+ // prune would delete a pre-existing trigger — and its access profile — with nothing
24075
+ // recording that it happened and nothing able to restore it. Opt out rather than grow the
24076
+ // snapshot a leg teardown could not honour anyway.
24077
+ pruneTriggers: false
21673
24078
  });
21674
24079
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
21675
24080
  assertions = await runAssertions(
@@ -21771,14 +24176,14 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
21771
24176
  if (err instanceof ScopeGateError) {
21772
24177
  process.stderr.write(` \u2716 Scope gate refused blueprint:
21773
24178
  `);
21774
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
24179
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
21775
24180
  `);
21776
24181
  return 2;
21777
24182
  }
21778
24183
  if (err instanceof CapabilityGateError) {
21779
24184
  process.stderr.write(` \u2716 Capability gate refused blueprint:
21780
24185
  `);
21781
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
24186
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
21782
24187
  `);
21783
24188
  return 2;
21784
24189
  }
@@ -22030,9 +24435,9 @@ function emitJoined(opts) {
22030
24435
 
22031
24436
  // src/build-info.ts
22032
24437
  var BUILD_INFO = {
22033
- cli: true ? "0.20.0" : "dev",
22034
- sdk: true ? "0.42.0" : "dev",
22035
- blueprints: true ? "0.17.0" : "dev"
24438
+ cli: true ? "0.21.0" : "dev",
24439
+ sdk: true ? "0.43.0" : "dev",
24440
+ blueprints: true ? "0.18.0" : "dev"
22036
24441
  };
22037
24442
  function formatBuildInfo() {
22038
24443
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -22043,6 +24448,9 @@ var KEY_FORMATS = ["human", "raw", "env", "json"];
22043
24448
  function collect(value, previous) {
22044
24449
  return [...previous, value];
22045
24450
  }
24451
+ function collectOptional(value, previous) {
24452
+ return [...previous ?? [], value];
24453
+ }
22046
24454
  function addAuthOptions(cmd) {
22047
24455
  return cmd.addOption(
22048
24456
  new Option("--env <env>", "target environment").choices(["staging", "production"]).default("production").hideHelp()
@@ -22114,6 +24522,7 @@ function buildProgram(deps = {}) {
22114
24522
  registerContextCommands(program, deps);
22115
24523
  registerIssuersCommands(program, deps);
22116
24524
  registerNamespaceCommands(program, deps);
24525
+ registerScriptsCommands(program, deps);
22117
24526
  registerAssumeCommand(program, deps);
22118
24527
  registerJoinCommand(program, deps);
22119
24528
  registerKeyringCommands(program);
@@ -22289,6 +24698,10 @@ function registerIssuersCommands(program, deps) {
22289
24698
  "opt-in self-service signup entry, repeatable (e.g. default:member) \u2014 no entry may target an elevated role",
22290
24699
  collect,
22291
24700
  []
24701
+ ).option(
24702
+ "--captured-claim <name>",
24703
+ "additional OIDC claim to capture from this issuer\u2019s tokens beyond the email claim, repeatable (e.g. phone_number); omit to capture nothing beyond email",
24704
+ collectOptional
22292
24705
  ).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22293
24706
  addCommonAuthOptions(register).action(
22294
24707
  async (flags) => {
@@ -22306,6 +24719,7 @@ function registerIssuersCommands(program, deps) {
22306
24719
  emailClaim: flags.emailClaim,
22307
24720
  userinfoUri: flags.userinfoUri,
22308
24721
  selfSignupPolicy: flags.selfSignupPolicy,
24722
+ capturedClaim: flags.capturedClaim,
22309
24723
  tenant
22310
24724
  },
22311
24725
  deps
@@ -22318,7 +24732,11 @@ function registerIssuersCommands(program, deps) {
22318
24732
  "replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
22319
24733
  collect,
22320
24734
  []
22321
- ).option("--clear-self-signup-policies", "disable self-signup entirely (sends an empty list; XOR --self-signup-policy)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
24735
+ ).option("--clear-self-signup-policies", "disable self-signup entirely (sends an empty list; XOR --self-signup-policy)").option(
24736
+ "--captured-claim <name>",
24737
+ "replace the additional OIDC claims captured beyond the email claim, repeatable \u2014 omit entirely to leave unchanged",
24738
+ collectOptional
24739
+ ).option("--clear-captured-claims", "capture nothing beyond the email claim (sends an empty list; XOR --captured-claim)").option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22322
24740
  addCommonAuthOptions(update).action(
22323
24741
  async (issuerId, flags) => {
22324
24742
  const tenant = parseTenantFlag(flags.tenant);
@@ -22334,6 +24752,8 @@ function registerIssuersCommands(program, deps) {
22334
24752
  status: flags.status,
22335
24753
  selfSignupPolicy: flags.selfSignupPolicy,
22336
24754
  clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
24755
+ capturedClaim: flags.capturedClaim,
24756
+ clearCapturedClaims: Boolean(flags.clearCapturedClaims),
22337
24757
  tenant
22338
24758
  },
22339
24759
  deps
@@ -22395,6 +24815,68 @@ function registerNamespaceCommands(program, deps) {
22395
24815
  }
22396
24816
  );
22397
24817
  }
24818
+ function registerScriptsCommands(program, deps) {
24819
+ const scripts = program.command("scripts").description("Manage script objects (the versioned JS source a trigger rule runs).");
24820
+ const push = scripts.command("push").description("Store a new version of a script name (always creates a version unless --if-changed).").argument("<name>", "script name: 1-64 chars of letters, digits, '_' and '-'").requiredOption("--context <c>", "app context to push into").requiredOption("--source-file <path>", "file holding the script source (max 300,000 UTF-8 bytes)").option("--input-contract <text>", "free-text description of the expected input shape (descriptive only)").option("--input-contract-file <path>", "read the input contract from a file instead").option("--if-changed", "push nothing when the current version already carries this exact content").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
24821
+ addCommonAuthOptions(push).action(
24822
+ async (name, flags) => {
24823
+ process.exit(
24824
+ await runScriptsPush(
24825
+ {
24826
+ ...toCommonOptions(flags),
24827
+ name,
24828
+ context: flags.context,
24829
+ sourceFile: flags.sourceFile,
24830
+ inputContract: flags.inputContract,
24831
+ inputContractFile: flags.inputContractFile,
24832
+ ifChanged: Boolean(flags.ifChanged),
24833
+ tenant: parseTenantFlag(flags.tenant)
24834
+ },
24835
+ deps
24836
+ )
24837
+ );
24838
+ }
24839
+ );
24840
+ const list = scripts.command("list").description("List script versions \u2014 every version of one --name (oldest first), or all scripts (newest first).").requiredOption("--context <c>", "app context to read").option("--name <name>", "list every version of this script name instead of a flat cross-name list").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
24841
+ addCommonAuthOptions(list).action(
24842
+ async (flags) => {
24843
+ process.exit(
24844
+ await runScriptsList(
24845
+ { ...toCommonOptions(flags), context: flags.context, name: flags.name, tenant: parseTenantFlag(flags.tenant) },
24846
+ deps
24847
+ )
24848
+ );
24849
+ }
24850
+ );
24851
+ const get = scripts.command("get").description("Show one script version, including its full source.").argument("<id>", "the script VERSION id (each version has its own)").requiredOption("--context <c>", "app context to read").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)");
24852
+ addCommonAuthOptions(get).action(
24853
+ async (id, flags) => {
24854
+ process.exit(
24855
+ await runScriptsGet(
24856
+ { ...toCommonOptions(flags), id, context: flags.context, tenant: parseTenantFlag(flags.tenant) },
24857
+ deps
24858
+ )
24859
+ );
24860
+ }
24861
+ );
24862
+ const del = scripts.command("delete").description("Permanently delete ONE script version (other versions of the name are unaffected).").argument("<id>", "the script VERSION id").requiredOption("--context <c>", "app context to delete from").option("--tenant <tenant>", "target tenant: 'test' or 'live' (default live)").option("--force", "skip the interactive confirmation (required in a non-interactive shell)");
24863
+ addCommonAuthOptions(del).action(
24864
+ async (id, flags) => {
24865
+ process.exit(
24866
+ await runScriptsDelete(
24867
+ {
24868
+ ...toCommonOptions(flags),
24869
+ id,
24870
+ context: flags.context,
24871
+ tenant: parseTenantFlag(flags.tenant),
24872
+ force: Boolean(flags.force)
24873
+ },
24874
+ deps
24875
+ )
24876
+ );
24877
+ }
24878
+ );
24879
+ }
22398
24880
  function registerRoleCommands(program, deps) {
22399
24881
  const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
22400
24882
  const create = role.command("create").description("Create (idempotent by roleId) a single-clause role from --actions.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id (3-31 chars; lowercase letter + lowercase/digit/dash)").requiredOption("--name <name>", "human-readable name").requiredOption("--actions <csv>", "allowed actions, comma-separated (e.g. records:cru,search:r)").option(
@@ -22403,72 +24885,106 @@ function registerRoleCommands(program, deps) {
22403
24885
  ).option(
22404
24886
  "--assumable <json>",
22405
24887
  `role-level \`vectros assume\` grant \u2014 JSON map of scope:<namespace> \u2192 allowed values (e.g. '{"scope:org":["orgB"]}'); independent of --data-scope, never data_scope's grammar (userId cannot be named)`
22406
- ).option("--description <text>", "free-text description");
24888
+ ).option(
24889
+ "--assignable-role <roleId>",
24890
+ 'restrict WHICH roles this clause may compose into a delegated profile, repeatable \u2014 omit for no restriction (the default); the platform rejects an empty list, so there is no way to spell "compose nothing"',
24891
+ collectOptional
24892
+ ).option("--description <text>", "free-text description").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
22407
24893
  addCommonAuthOptions(create).action(
22408
24894
  async (flags) => {
22409
- process.exit(
22410
- await runRoleCreate({
22411
- ...toCommonOptions(flags),
22412
- context: flags.context,
22413
- roleId: flags.roleId,
22414
- name: flags.name,
22415
- actions: flags.actions,
22416
- dataScope: flags.dataScope,
22417
- assumable: flags.assumable,
22418
- description: flags.description
22419
- }, deps)
22420
- );
24895
+ process.exitCode = await runRoleCreate({
24896
+ ...toCommonOptions(flags),
24897
+ tenant: parseTenantFlag(flags.tenant),
24898
+ context: flags.context,
24899
+ roleId: flags.roleId,
24900
+ name: flags.name,
24901
+ actions: flags.actions,
24902
+ dataScope: flags.dataScope,
24903
+ assumable: flags.assumable,
24904
+ assignableRole: flags.assignableRole,
24905
+ description: flags.description
24906
+ }, deps);
22421
24907
  }
22422
24908
  );
22423
- const list = role.command("list").description("List the roles defined in a context.").requiredOption("--context <c>", "app context");
24909
+ const list = role.command("list").description("List the roles defined in a context.").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
22424
24910
  addCommonAuthOptions(list).action(async (flags) => {
22425
- process.exit(await runRoleList({ ...toCommonOptions(flags), context: flags.context }, deps));
22426
- });
22427
- const get = role.command("get").description("Show one role by id.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id");
22428
- addCommonAuthOptions(get).action(async (flags) => {
22429
- process.exit(await runRoleGet({ ...toCommonOptions(flags), context: flags.context, roleId: flags.roleId }, deps));
22430
- });
22431
- const del = role.command("delete").description("Delete a role (referencing profiles must be reassigned).").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id");
22432
- addAuthOptions(del).action(async (flags) => {
22433
- process.exit(await runRoleDelete({ ...toCommonOptions(flags), context: flags.context, roleId: flags.roleId }, deps));
24911
+ process.exitCode = await runRoleList(
24912
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context },
24913
+ deps
24914
+ );
22434
24915
  });
24916
+ const get = role.command("get").description("Show one role by id.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24917
+ addCommonAuthOptions(get).action(
24918
+ async (flags) => {
24919
+ process.exitCode = await runRoleGet(
24920
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, roleId: flags.roleId },
24921
+ deps
24922
+ );
24923
+ }
24924
+ );
24925
+ const del = role.command("delete").description("Delete a role (referencing profiles must be reassigned).").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24926
+ addAuthOptions(del).action(
24927
+ async (flags) => {
24928
+ process.exitCode = await runRoleDelete(
24929
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, roleId: flags.roleId },
24930
+ deps
24931
+ );
24932
+ }
24933
+ );
22435
24934
  }
22436
24935
  function registerAccessCommands(program, deps) {
22437
24936
  const access2 = program.command("access").description("Manage access bindings: principal \u2194 context \u2194 role/scopes.");
22438
- const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (repeatable \u2014 2+ compose additively; XOR --actions)", collect, []).option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)").option("--scope <ns:value>", "identity override: a scope the principal acts under (repeatable, e.g. org:<id>)", collect, []).option("--identity-overrides <json>", "general identity overrides as a JSON object (merged over --scope)");
24937
+ const grant = access2.command("grant").description("Bind a principal to a context via a role or inline actions.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--role <roleId>", "reference a Role (repeatable \u2014 2+ compose additively; XOR --actions)", collect, []).option("--actions <csv>", "inline single-clause actions, comma-separated (XOR --role)").option("--scope <ns:value>", "identity override: a scope the principal acts under (repeatable, e.g. org:<id>)", collect, []).option("--identity-overrides <json>", "general identity overrides as a JSON object (merged over --scope)").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
22439
24938
  addCommonAuthOptions(grant).action(
22440
24939
  async (flags) => {
22441
- process.exit(
22442
- await runAccessGrant({
22443
- ...toCommonOptions(flags),
22444
- principal: flags.principal,
22445
- context: flags.context,
22446
- role: flags.role,
22447
- actions: flags.actions,
22448
- scope: flags.scope,
22449
- identityOverrides: flags.identityOverrides
22450
- }, deps)
24940
+ process.exitCode = await runAccessGrant({
24941
+ ...toCommonOptions(flags),
24942
+ tenant: parseTenantFlag(flags.tenant),
24943
+ principal: flags.principal,
24944
+ context: flags.context,
24945
+ role: flags.role,
24946
+ actions: flags.actions,
24947
+ scope: flags.scope,
24948
+ identityOverrides: flags.identityOverrides
24949
+ }, deps);
24950
+ }
24951
+ );
24952
+ const revoke = access2.command("revoke").description("Remove a principal\u2019s binding in a context.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24953
+ addAuthOptions(revoke).action(
24954
+ async (flags) => {
24955
+ process.exitCode = await runAccessRevoke(
24956
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
24957
+ deps
24958
+ );
24959
+ }
24960
+ );
24961
+ const list = access2.command("list").description("List a context\u2019s members (--context) or a principal\u2019s contexts (--principal).").option("--context <c>", "list members of this context").option("--principal <p>", "list contexts this principal is bound to ('me' = your own user)").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24962
+ addCommonAuthOptions(list).action(
24963
+ async (flags) => {
24964
+ process.exitCode = await runAccessList(
24965
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, principal: flags.principal },
24966
+ deps
24967
+ );
24968
+ }
24969
+ );
24970
+ const get = access2.command("get").description("Show one principal\u2019s binding in a context.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24971
+ addCommonAuthOptions(get).action(
24972
+ async (flags) => {
24973
+ process.exitCode = await runAccessGet(
24974
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
24975
+ deps
24976
+ );
24977
+ }
24978
+ );
24979
+ const explain = access2.command("explain").description("Show what a principal\u2019s binding grants: its clauses + a concrete access matrix.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
24980
+ addCommonAuthOptions(explain).action(
24981
+ async (flags) => {
24982
+ process.exitCode = await runAccessExplain(
24983
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
24984
+ deps
22451
24985
  );
22452
24986
  }
22453
24987
  );
22454
- const revoke = access2.command("revoke").description("Remove a principal\u2019s binding in a context.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context");
22455
- addAuthOptions(revoke).action(async (flags) => {
22456
- process.exit(await runAccessRevoke({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps));
22457
- });
22458
- const list = access2.command("list").description("List a context\u2019s members (--context) or a principal\u2019s contexts (--principal).").option("--context <c>", "list members of this context").option("--principal <p>", "list contexts this principal is bound to ('me' = your own user)");
22459
- addCommonAuthOptions(list).action(async (flags) => {
22460
- process.exit(await runAccessList({ ...toCommonOptions(flags), context: flags.context, principal: flags.principal }, deps));
22461
- });
22462
- const get = access2.command("get").description("Show one principal\u2019s binding in a context.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context");
22463
- addCommonAuthOptions(get).action(async (flags) => {
22464
- process.exit(await runAccessGet({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps));
22465
- });
22466
- const explain = access2.command("explain").description("Show what a principal\u2019s binding grants: its clauses + a concrete access matrix.").requiredOption("--principal <p>", "principal: 'usr_<userId>', 'key_<keyId>', or 'me' (your own user)").requiredOption("--context <c>", "app context");
22467
- addCommonAuthOptions(explain).action(async (flags) => {
22468
- process.exit(
22469
- await runAccessExplain({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps)
22470
- );
22471
- });
22472
24988
  }
22473
24989
  function dimensionOption() {
22474
24990
  return new Option(