@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.js CHANGED
@@ -521,8 +521,8 @@ var require_graceful_fs = __commonJS({
521
521
  fs5.createReadStream = createReadStream;
522
522
  fs5.createWriteStream = createWriteStream;
523
523
  var fs$readFile = fs5.readFile;
524
- fs5.readFile = readFile3;
525
- function readFile3(path4, options, cb) {
524
+ fs5.readFile = readFile4;
525
+ function readFile4(path4, options, cb) {
526
526
  if (typeof options === "function")
527
527
  cb = options, options = null;
528
528
  return go$readFile(path4, options, cb);
@@ -1614,7 +1614,7 @@ var require_proper_lockfile = __commonJS({
1614
1614
  // src/cli-bootstrap.ts
1615
1615
  var readline2 = __toESM(require("readline/promises"));
1616
1616
 
1617
- // node_modules/@vectros-ai/sdk/dist/index.mjs
1617
+ // ../../node_modules/@vectros-ai/sdk/dist/index.mjs
1618
1618
  var __defProp2 = Object.defineProperty;
1619
1619
  var __export = (target, all) => {
1620
1620
  for (var name in all)
@@ -1648,6 +1648,7 @@ __export(api_exports, {
1648
1648
  FieldDef: () => FieldDef,
1649
1649
  FileUploadRequest: () => FileUploadRequest,
1650
1650
  ForbiddenError: () => ForbiddenError,
1651
+ GatewayTimeoutError: () => GatewayTimeoutError,
1651
1652
  IdentityLookupRequest: () => IdentityLookupRequest,
1652
1653
  IndexFailure: () => IndexFailure,
1653
1654
  InternalServerError: () => InternalServerError,
@@ -1672,9 +1673,12 @@ __export(api_exports, {
1672
1673
  SearchRequest: () => SearchRequest,
1673
1674
  SearchResult: () => SearchResult,
1674
1675
  SearchResultsEvent: () => SearchResultsEvent,
1676
+ ServiceUnavailableError: () => ServiceUnavailableError,
1675
1677
  TooManyRequestsError: () => TooManyRequestsError,
1678
+ TriggerFailureResponse: () => TriggerFailureResponse,
1676
1679
  TruncationWarningEvent: () => TruncationWarningEvent,
1677
1680
  UnauthorizedError: () => UnauthorizedError,
1681
+ UnprocessableEntityError: () => UnprocessableEntityError,
1678
1682
  UserExistsResponse: () => UserExistsResponse,
1679
1683
  UserRequest: () => UserRequest,
1680
1684
  UserResponse: () => UserResponse,
@@ -1686,7 +1690,9 @@ __export(api_exports, {
1686
1690
  inference: () => inference_exports,
1687
1691
  records: () => records_exports,
1688
1692
  schemas: () => schemas_exports,
1689
- search: () => search_exports
1693
+ scripts: () => scripts_exports,
1694
+ search: () => search_exports,
1695
+ triggers: () => triggers_exports
1690
1696
  });
1691
1697
  var toJson = (value, replacer, space) => {
1692
1698
  return JSON.stringify(value, replacer, space);
@@ -1806,6 +1812,21 @@ var ForbiddenError = class extends VectrosError {
1806
1812
  this.name = this.constructor.name;
1807
1813
  }
1808
1814
  };
1815
+ var GatewayTimeoutError = class extends VectrosError {
1816
+ constructor(body, rawResponse) {
1817
+ super({
1818
+ message: "GatewayTimeoutError",
1819
+ statusCode: 504,
1820
+ body,
1821
+ rawResponse
1822
+ });
1823
+ Object.setPrototypeOf(this, new.target.prototype);
1824
+ if (Error.captureStackTrace) {
1825
+ Error.captureStackTrace(this, this.constructor);
1826
+ }
1827
+ this.name = this.constructor.name;
1828
+ }
1829
+ };
1809
1830
  var InternalServerError = class extends VectrosError {
1810
1831
  constructor(body, rawResponse) {
1811
1832
  super({
@@ -1866,6 +1887,21 @@ var PaymentRequiredError = class extends VectrosError {
1866
1887
  this.name = this.constructor.name;
1867
1888
  }
1868
1889
  };
1890
+ var ServiceUnavailableError = class extends VectrosError {
1891
+ constructor(body, rawResponse) {
1892
+ super({
1893
+ message: "ServiceUnavailableError",
1894
+ statusCode: 503,
1895
+ body,
1896
+ rawResponse
1897
+ });
1898
+ Object.setPrototypeOf(this, new.target.prototype);
1899
+ if (Error.captureStackTrace) {
1900
+ Error.captureStackTrace(this, this.constructor);
1901
+ }
1902
+ this.name = this.constructor.name;
1903
+ }
1904
+ };
1869
1905
  var TooManyRequestsError = class extends VectrosError {
1870
1906
  constructor(body, rawResponse) {
1871
1907
  super({
@@ -1896,6 +1932,21 @@ var UnauthorizedError = class extends VectrosError {
1896
1932
  this.name = this.constructor.name;
1897
1933
  }
1898
1934
  };
1935
+ var UnprocessableEntityError = class extends VectrosError {
1936
+ constructor(body, rawResponse) {
1937
+ super({
1938
+ message: "UnprocessableEntityError",
1939
+ statusCode: 422,
1940
+ body,
1941
+ rawResponse
1942
+ });
1943
+ Object.setPrototypeOf(this, new.target.prototype);
1944
+ if (Error.captureStackTrace) {
1945
+ Error.captureStackTrace(this, this.constructor);
1946
+ }
1947
+ this.name = this.constructor.name;
1948
+ }
1949
+ };
1899
1950
  var auth_exports = {};
1900
1951
  var ErasureRequest;
1901
1952
  ((ErasureRequest2) => {
@@ -1985,6 +2036,7 @@ var LookupRecordsRequestOrder = {
1985
2036
  Desc: "desc"
1986
2037
  };
1987
2038
  var schemas_exports = {};
2039
+ var scripts_exports = {};
1988
2040
  var SearchRequest;
1989
2041
  ((SearchRequest2) => {
1990
2042
  SearchRequest2.Mode = {
@@ -2010,6 +2062,7 @@ var search_exports = {};
2010
2062
  __export(search_exports, {
2011
2063
  SearchRequest: () => SearchRequest
2012
2064
  });
2065
+ var triggers_exports = {};
2013
2066
  var AccessProfileRequest;
2014
2067
  ((AccessProfileRequest2) => {
2015
2068
  AccessProfileRequest2.Status = {
@@ -2039,7 +2092,9 @@ var BatchWriteResult;
2039
2092
  Created: "created",
2040
2093
  Updated: "updated",
2041
2094
  Conflict: "conflict",
2042
- Invalid: "invalid"
2095
+ Invalid: "invalid",
2096
+ Forbidden: "forbidden",
2097
+ NotCommitted: "not_committed"
2043
2098
  };
2044
2099
  })(BatchWriteResult || (BatchWriteResult = {}));
2045
2100
  var ChatMessage;
@@ -2343,13 +2398,23 @@ var SearchResultsEvent;
2343
2398
  SearchResults: "search_results"
2344
2399
  };
2345
2400
  })(SearchResultsEvent || (SearchResultsEvent = {}));
2401
+ var TriggerFailureResponse;
2402
+ ((TriggerFailureResponse2) => {
2403
+ TriggerFailureResponse2.Event = {
2404
+ Create: "CREATE",
2405
+ Update: "UPDATE",
2406
+ Delete: "DELETE"
2407
+ };
2408
+ })(TriggerFailureResponse || (TriggerFailureResponse = {}));
2346
2409
  var TruncationWarningEvent;
2347
2410
  ((TruncationWarningEvent2) => {
2348
2411
  TruncationWarningEvent2.Event = {
2349
2412
  TruncationWarning: "truncation_warning"
2350
2413
  };
2351
2414
  TruncationWarningEvent2.Reason = {
2352
- ContextWindowBudget: "context_window_budget"
2415
+ ContextWindowBudget: "context_window_budget",
2416
+ NoGroundableContent: "no_groundable_content",
2417
+ ContextWindowBudgetAndNoContent: "context_window_budget_and_no_content"
2353
2418
  };
2354
2419
  })(TruncationWarningEvent || (TruncationWarningEvent = {}));
2355
2420
  var UserExistsResponse;
@@ -2364,8 +2429,7 @@ var UserRequest;
2364
2429
  ((UserRequest2) => {
2365
2430
  UserRequest2.Status = {
2366
2431
  Active: "ACTIVE",
2367
- Suspended: "SUSPENDED",
2368
- Pending: "PENDING"
2432
+ Suspended: "SUSPENDED"
2369
2433
  };
2370
2434
  UserRequest2.Type = {
2371
2435
  Human: "HUMAN",
@@ -3833,6 +3897,16 @@ function mergeHeaders(...headersArray) {
3833
3897
  }
3834
3898
  return result;
3835
3899
  }
3900
+ function mergeOnlyDefinedHeaders(...headersArray) {
3901
+ const result = {};
3902
+ for (const [key, value] of headersArray.filter((headers) => headers != null).flatMap((headers) => Object.entries(headers))) {
3903
+ const insensitiveKey = key.toLowerCase();
3904
+ if (value != null) {
3905
+ result[insensitiveKey] = value;
3906
+ }
3907
+ }
3908
+ return result;
3909
+ }
3836
3910
  function normalizeClientOptions(options) {
3837
3911
  const headers = mergeHeaders(
3838
3912
  {
@@ -4103,7 +4177,7 @@ var AuthClient = class {
4103
4177
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/keys/scoped");
4104
4178
  }
4105
4179
  /**
4106
- * 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.
4180
+ * 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.
4107
4181
  *
4108
4182
  * @param {Vectros.CreateScopedKeyRequest} request
4109
4183
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4111,6 +4185,7 @@ var AuthClient = class {
4111
4185
  * @throws {@link Vectros.BadRequestError}
4112
4186
  * @throws {@link Vectros.ForbiddenError}
4113
4187
  * @throws {@link Vectros.NotFoundError}
4188
+ * @throws {@link Vectros.ConflictError}
4114
4189
  * @throws {@link Vectros.TooManyRequestsError}
4115
4190
  *
4116
4191
  * @example
@@ -4159,6 +4234,8 @@ var AuthClient = class {
4159
4234
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
4160
4235
  case 404:
4161
4236
  throw new NotFoundError(_response.error.body, _response.rawResponse);
4237
+ case 409:
4238
+ throw new ConflictError(_response.error.body, _response.rawResponse);
4162
4239
  case 429:
4163
4240
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
4164
4241
  default:
@@ -4570,7 +4647,7 @@ var AuthClient = class {
4570
4647
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/app-contexts");
4571
4648
  }
4572
4649
  /**
4573
- * 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.
4650
+ * 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.
4574
4651
  *
4575
4652
  * @param {Vectros.CreateAppContextRequest} request
4576
4653
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5054,7 +5131,7 @@ var AuthClient = class {
5054
5131
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/app-contexts/{contextId}");
5055
5132
  }
5056
5133
  /**
5057
- * 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.
5134
+ * 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.
5058
5135
  *
5059
5136
  * @param {Vectros.UpdateAppContextRequest} request
5060
5137
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5536,7 +5613,7 @@ var AuthClient = class {
5536
5613
  );
5537
5614
  }
5538
5615
  /**
5539
- * 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.
5616
+ * 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`.
5540
5617
  *
5541
5618
  * @param {Vectros.GetUsageRequest} request
5542
5619
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7670,7 +7747,7 @@ var FoldersClient = class {
7670
7747
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/folders/{id}");
7671
7748
  }
7672
7749
  /**
7673
- * 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.
7750
+ * 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.
7674
7751
  *
7675
7752
  * @param {Vectros.DeleteFolderRequest} request
7676
7753
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8079,11 +8156,12 @@ var IdentityClient = class {
8079
8156
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/entities/{namespace}/{id}");
8080
8157
  }
8081
8158
  /**
8082
- * 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.
8159
+ * 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.
8083
8160
  *
8084
8161
  * @param {Vectros.UpdateEntityRequest} request
8085
8162
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8086
8163
  *
8164
+ * @throws {@link Vectros.BadRequestError}
8087
8165
  * @throws {@link Vectros.NotFoundError}
8088
8166
  * @throws {@link Vectros.TooManyRequestsError}
8089
8167
  *
@@ -8133,6 +8211,8 @@ var IdentityClient = class {
8133
8211
  }
8134
8212
  if (_response.error.reason === "status-code") {
8135
8213
  switch (_response.error.statusCode) {
8214
+ case 400:
8215
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
8136
8216
  case 404:
8137
8217
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8138
8218
  case 429:
@@ -9357,7 +9437,7 @@ var InferenceClient = class {
9357
9437
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/documents/{id}/ask");
9358
9438
  }
9359
9439
  /**
9360
- * 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.
9440
+ * 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.
9361
9441
  */
9362
9442
  ragInference(request, requestOptions) {
9363
9443
  return HttpResponsePromise.fromPromise(this.__ragInference(request, requestOptions));
@@ -9552,14 +9632,18 @@ var RecordsClient = class {
9552
9632
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/records/lookup/batch");
9553
9633
  }
9554
9634
  /**
9555
- * 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.
9635
+ * 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.
9636
+ *
9637
+ * `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.
9638
+ *
9639
+ * 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.
9556
9640
  *
9557
9641
  * @param {Vectros.BatchWriteRequest} request
9558
9642
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9559
9643
  *
9644
+ * @throws {@link Vectros.BadRequestError}
9560
9645
  * @throws {@link Vectros.ForbiddenError}
9561
9646
  * @throws {@link Vectros.TooManyRequestsError}
9562
- * @throws {@link Vectros.NotImplementedError}
9563
9647
  *
9564
9648
  * @example
9565
9649
  * await client.records.batchWriteRecords()
@@ -9568,6 +9652,11 @@ var RecordsClient = class {
9568
9652
  return HttpResponsePromise.fromPromise(this.__batchWriteRecords(request, requestOptions));
9569
9653
  }
9570
9654
  async __batchWriteRecords(request = {}, requestOptions) {
9655
+ const { upsert, allowClear, ..._body } = request;
9656
+ const _queryParams = {
9657
+ upsert,
9658
+ allowClear
9659
+ };
9571
9660
  const _authRequest = await this._options.authProvider.getAuthRequest();
9572
9661
  const _headers = mergeHeaders(
9573
9662
  _authRequest.headers,
@@ -9582,9 +9671,9 @@ var RecordsClient = class {
9582
9671
  method: "POST",
9583
9672
  headers: _headers,
9584
9673
  contentType: "application/json",
9585
- queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
9674
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
9586
9675
  requestType: "json",
9587
- body: request,
9676
+ body: _body,
9588
9677
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
9589
9678
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
9590
9679
  abortSignal: requestOptions?.abortSignal,
@@ -9596,12 +9685,12 @@ var RecordsClient = class {
9596
9685
  }
9597
9686
  if (_response.error.reason === "status-code") {
9598
9687
  switch (_response.error.statusCode) {
9688
+ case 400:
9689
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
9599
9690
  case 403:
9600
9691
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
9601
9692
  case 429:
9602
9693
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
9603
- case 501:
9604
- throw new NotImplementedError(_response.error.body, _response.rawResponse);
9605
9694
  default:
9606
9695
  throw new VectrosError({
9607
9696
  statusCode: _response.error.statusCode,
@@ -10504,7 +10593,7 @@ var SchemasClient = class {
10504
10593
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/schemas/{id}");
10505
10594
  }
10506
10595
  /**
10507
- * 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.
10596
+ * 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.
10508
10597
  *
10509
10598
  * @param {Vectros.UpdateSchemaRequest} request
10510
10599
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10512,6 +10601,7 @@ var SchemasClient = class {
10512
10601
  * @throws {@link Vectros.BadRequestError}
10513
10602
  * @throws {@link Vectros.ForbiddenError}
10514
10603
  * @throws {@link Vectros.NotFoundError}
10604
+ * @throws {@link Vectros.ConflictError}
10515
10605
  * @throws {@link Vectros.TooManyRequestsError}
10516
10606
  *
10517
10607
  * @example
@@ -10563,6 +10653,8 @@ var SchemasClient = class {
10563
10653
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
10564
10654
  case 404:
10565
10655
  throw new NotFoundError(_response.error.body, _response.rawResponse);
10656
+ case 409:
10657
+ throw new ConflictError(_response.error.body, _response.rawResponse);
10566
10658
  case 429:
10567
10659
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
10568
10660
  default:
@@ -10576,7 +10668,7 @@ var SchemasClient = class {
10576
10668
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/schemas/{id}");
10577
10669
  }
10578
10670
  /**
10579
- * 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.
10671
+ * 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.
10580
10672
  *
10581
10673
  * @param {Vectros.DeleteSchemaRequest} request
10582
10674
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10698,46 +10790,70 @@ var SchemasClient = class {
10698
10790
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/schemas/{id}/versions");
10699
10791
  }
10700
10792
  };
10701
- var SearchClient = class {
10793
+ var ScriptsClient = class {
10702
10794
  constructor(options) {
10703
10795
  this._options = normalizeClientOptionsWithAuth(options);
10704
10796
  }
10705
10797
  /**
10706
- * 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.
10798
+ * 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.
10707
10799
  *
10708
- * @param {Vectros.SearchRequest} request
10709
- * @param {SearchClient.RequestOptions} requestOptions - Request-specific configuration.
10800
+ * **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.
10801
+ *
10802
+ * **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.
10803
+ *
10804
+ * **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`).
10805
+ *
10806
+ * **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()`.
10807
+ *
10808
+ * **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.
10809
+ *
10810
+ * @param {Vectros.ScriptExecuteRequest} request
10811
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
10710
10812
  *
10711
10813
  * @throws {@link Vectros.BadRequestError}
10814
+ * @throws {@link Vectros.PaymentRequiredError}
10712
10815
  * @throws {@link Vectros.ForbiddenError}
10816
+ * @throws {@link Vectros.NotFoundError}
10817
+ * @throws {@link Vectros.ConflictError}
10818
+ * @throws {@link Vectros.ContentTooLargeError}
10819
+ * @throws {@link Vectros.UnprocessableEntityError}
10713
10820
  * @throws {@link Vectros.TooManyRequestsError}
10821
+ * @throws {@link Vectros.InternalServerError}
10822
+ * @throws {@link Vectros.ServiceUnavailableError}
10823
+ * @throws {@link Vectros.GatewayTimeoutError}
10714
10824
  *
10715
10825
  * @example
10716
- * await client.search.content({
10717
- * query: "patient intake form diabetes"
10826
+ * await client.scripts.executeScript({
10827
+ * "Idempotency-Key": "order-8412-attempt",
10828
+ * scriptRef: {
10829
+ * name: "notify-assignee",
10830
+ * version: "3"
10831
+ * }
10718
10832
  * })
10719
10833
  */
10720
- content(request, requestOptions) {
10721
- return HttpResponsePromise.fromPromise(this.__content(request, requestOptions));
10834
+ executeScript(request, requestOptions) {
10835
+ return HttpResponsePromise.fromPromise(this.__executeScript(request, requestOptions));
10722
10836
  }
10723
- async __content(request, requestOptions) {
10837
+ async __executeScript(request, requestOptions) {
10838
+ const { "Idempotency-Key": idempotencyKey, ..._body } = request;
10724
10839
  const _authRequest = await this._options.authProvider.getAuthRequest();
10725
10840
  const _headers = mergeHeaders(
10726
10841
  _authRequest.headers,
10727
10842
  this._options?.headers,
10843
+ mergeOnlyDefinedHeaders({ "Idempotency-Key": idempotencyKey }),
10728
10844
  requestOptions?.headers
10729
10845
  );
10730
10846
  const _response = await fetcher({
10731
10847
  url: url_exports.join(
10732
10848
  await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
10733
- "v1/search"
10849
+ "v1/scripts/execute"
10734
10850
  ),
10735
10851
  method: "POST",
10736
10852
  headers: _headers,
10737
10853
  contentType: "application/json",
10738
10854
  queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
10739
10855
  requestType: "json",
10740
- body: request,
10856
+ body: _body,
10741
10857
  timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
10742
10858
  maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
10743
10859
  abortSignal: requestOptions?.abortSignal,
@@ -10751,10 +10867,26 @@ var SearchClient = class {
10751
10867
  switch (_response.error.statusCode) {
10752
10868
  case 400:
10753
10869
  throw new BadRequestError(_response.error.body, _response.rawResponse);
10870
+ case 402:
10871
+ throw new PaymentRequiredError(_response.error.body, _response.rawResponse);
10754
10872
  case 403:
10755
10873
  throw new ForbiddenError(_response.error.body, _response.rawResponse);
10874
+ case 404:
10875
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
10876
+ case 409:
10877
+ throw new ConflictError(_response.error.body, _response.rawResponse);
10878
+ case 413:
10879
+ throw new ContentTooLargeError(_response.error.body, _response.rawResponse);
10880
+ case 422:
10881
+ throw new UnprocessableEntityError(_response.error.body, _response.rawResponse);
10756
10882
  case 429:
10757
10883
  throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
10884
+ case 500:
10885
+ throw new InternalServerError(_response.error.body, _response.rawResponse);
10886
+ case 503:
10887
+ throw new ServiceUnavailableError(_response.error.body, _response.rawResponse);
10888
+ case 504:
10889
+ throw new GatewayTimeoutError(_response.error.body, _response.rawResponse);
10758
10890
  default:
10759
10891
  throw new VectrosError({
10760
10892
  statusCode: _response.error.statusCode,
@@ -10763,71 +10895,834 @@ var SearchClient = class {
10763
10895
  });
10764
10896
  }
10765
10897
  }
10766
- return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/search");
10767
- }
10768
- };
10769
- var VectrosClient = class {
10770
- constructor(options) {
10771
- this._options = normalizeClientOptionsWithAuth(options);
10772
- }
10773
- get auth() {
10774
- return this._auth ?? (this._auth = new AuthClient(this._options));
10775
- }
10776
- get documents() {
10777
- return this._documents ?? (this._documents = new DocumentsClient(this._options));
10778
- }
10779
- get identity() {
10780
- return this._identity ?? (this._identity = new IdentityClient(this._options));
10781
- }
10782
- get compliance() {
10783
- return this._compliance ?? (this._compliance = new ComplianceClient(this._options));
10784
- }
10785
- get folders() {
10786
- return this._folders ?? (this._folders = new FoldersClient(this._options));
10787
- }
10788
- get inference() {
10789
- return this._inference ?? (this._inference = new InferenceClient(this._options));
10898
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/scripts/execute");
10790
10899
  }
10791
- get records() {
10792
- return this._records ?? (this._records = new RecordsClient(this._options));
10793
- }
10794
- get schemas() {
10795
- return this._schemas ?? (this._schemas = new SchemasClient(this._options));
10900
+ /**
10901
+ * 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.
10902
+ *
10903
+ * @param {Vectros.ListScriptsRequest} request
10904
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
10905
+ *
10906
+ * @example
10907
+ * await client.scripts.listScripts({
10908
+ * name: "notify-assignee",
10909
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
10910
+ * })
10911
+ */
10912
+ listScripts(request = {}, requestOptions) {
10913
+ return HttpResponsePromise.fromPromise(this.__listScripts(request, requestOptions));
10796
10914
  }
10797
- get search() {
10798
- return this._search ?? (this._search = new SearchClient(this._options));
10915
+ async __listScripts(request = {}, requestOptions) {
10916
+ const { name, startFrom, limit } = request;
10917
+ const _queryParams = {
10918
+ name,
10919
+ startFrom,
10920
+ limit
10921
+ };
10922
+ const _authRequest = await this._options.authProvider.getAuthRequest();
10923
+ const _headers = mergeHeaders(
10924
+ _authRequest.headers,
10925
+ this._options?.headers,
10926
+ requestOptions?.headers
10927
+ );
10928
+ const _response = await fetcher({
10929
+ url: url_exports.join(
10930
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
10931
+ "v1/scripts"
10932
+ ),
10933
+ method: "GET",
10934
+ headers: _headers,
10935
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
10936
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
10937
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
10938
+ abortSignal: requestOptions?.abortSignal,
10939
+ fetchFn: this._options?.fetch,
10940
+ logging: this._options.logging
10941
+ });
10942
+ if (_response.ok) {
10943
+ return { data: _response.body, rawResponse: _response.rawResponse };
10944
+ }
10945
+ if (_response.error.reason === "status-code") {
10946
+ throw new VectrosError({
10947
+ statusCode: _response.error.statusCode,
10948
+ body: _response.error.body,
10949
+ rawResponse: _response.rawResponse
10950
+ });
10951
+ }
10952
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/scripts");
10799
10953
  }
10800
10954
  /**
10801
- * Make a passthrough request using the SDK's configured auth, retry, logging, etc.
10802
- * This is useful for making requests to endpoints not yet supported in the SDK.
10803
- * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL.
10955
+ * 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.
10804
10956
  *
10805
- * @param {Request | string | URL} input - The URL, path, or Request object.
10806
- * @param {RequestInit} init - Standard fetch RequestInit options.
10807
- * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal).
10808
- * @returns {Promise<Response>} A standard Response object.
10957
+ * @param {Vectros.ScriptRequest} request
10958
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
10959
+ *
10960
+ * @throws {@link Vectros.BadRequestError}
10961
+ * @throws {@link Vectros.ForbiddenError}
10962
+ * @throws {@link Vectros.ConflictError}
10963
+ * @throws {@link Vectros.TooManyRequestsError}
10964
+ *
10965
+ * @example
10966
+ * await client.scripts.createScript({
10967
+ * name: "notify-assignee",
10968
+ * source: "export default function(input) { return { ok: true }; }"
10969
+ * })
10809
10970
  */
10810
- async fetch(input, init, requestOptions) {
10811
- return makePassthroughRequest(
10812
- input,
10813
- init,
10814
- {
10815
- baseUrl: this._options.baseUrl ?? this._options.environment,
10816
- headers: this._options.headers,
10817
- timeoutInSeconds: this._options.timeoutInSeconds,
10818
- maxRetries: this._options.maxRetries,
10819
- fetch: this._options.fetch,
10820
- logging: this._options.logging,
10821
- getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers
10822
- },
10823
- requestOptions
10971
+ createScript(request, requestOptions) {
10972
+ return HttpResponsePromise.fromPromise(this.__createScript(request, requestOptions));
10973
+ }
10974
+ async __createScript(request, requestOptions) {
10975
+ const _authRequest = await this._options.authProvider.getAuthRequest();
10976
+ const _headers = mergeHeaders(
10977
+ _authRequest.headers,
10978
+ this._options?.headers,
10979
+ requestOptions?.headers
10824
10980
  );
10981
+ const _response = await fetcher({
10982
+ url: url_exports.join(
10983
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
10984
+ "v1/scripts"
10985
+ ),
10986
+ method: "POST",
10987
+ headers: _headers,
10988
+ contentType: "application/json",
10989
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
10990
+ requestType: "json",
10991
+ body: request,
10992
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
10993
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
10994
+ abortSignal: requestOptions?.abortSignal,
10995
+ fetchFn: this._options?.fetch,
10996
+ logging: this._options.logging
10997
+ });
10998
+ if (_response.ok) {
10999
+ return { data: _response.body, rawResponse: _response.rawResponse };
11000
+ }
11001
+ if (_response.error.reason === "status-code") {
11002
+ switch (_response.error.statusCode) {
11003
+ case 400:
11004
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
11005
+ case 403:
11006
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
11007
+ case 409:
11008
+ throw new ConflictError(_response.error.body, _response.rawResponse);
11009
+ case 429:
11010
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11011
+ default:
11012
+ throw new VectrosError({
11013
+ statusCode: _response.error.statusCode,
11014
+ body: _response.error.body,
11015
+ rawResponse: _response.rawResponse
11016
+ });
11017
+ }
11018
+ }
11019
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/scripts");
10825
11020
  }
10826
- };
10827
- var logging;
10828
- ((logging2) => {
10829
- logging2.LogLevel = LogLevel;
10830
- logging2.ConsoleLogger = ConsoleLogger;
11021
+ /**
11022
+ * Retrieves a single script version by its ID. Requires the `scripts:r` scope.
11023
+ *
11024
+ * @param {Vectros.GetScriptRequest} request
11025
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
11026
+ *
11027
+ * @throws {@link Vectros.NotFoundError}
11028
+ *
11029
+ * @example
11030
+ * await client.scripts.getScript({
11031
+ * id: "id"
11032
+ * })
11033
+ */
11034
+ getScript(request, requestOptions) {
11035
+ return HttpResponsePromise.fromPromise(this.__getScript(request, requestOptions));
11036
+ }
11037
+ async __getScript(request, requestOptions) {
11038
+ const { id } = request;
11039
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11040
+ const _headers = mergeHeaders(
11041
+ _authRequest.headers,
11042
+ this._options?.headers,
11043
+ requestOptions?.headers
11044
+ );
11045
+ const _response = await fetcher({
11046
+ url: url_exports.join(
11047
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11048
+ `v1/scripts/${url_exports.encodePathParam(id)}`
11049
+ ),
11050
+ method: "GET",
11051
+ headers: _headers,
11052
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11053
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11054
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11055
+ abortSignal: requestOptions?.abortSignal,
11056
+ fetchFn: this._options?.fetch,
11057
+ logging: this._options.logging
11058
+ });
11059
+ if (_response.ok) {
11060
+ return { data: _response.body, rawResponse: _response.rawResponse };
11061
+ }
11062
+ if (_response.error.reason === "status-code") {
11063
+ switch (_response.error.statusCode) {
11064
+ case 404:
11065
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
11066
+ default:
11067
+ throw new VectrosError({
11068
+ statusCode: _response.error.statusCode,
11069
+ body: _response.error.body,
11070
+ rawResponse: _response.rawResponse
11071
+ });
11072
+ }
11073
+ }
11074
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/scripts/{id}");
11075
+ }
11076
+ /**
11077
+ * 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.
11078
+ *
11079
+ * @param {Vectros.UpdateScriptRequest} request
11080
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
11081
+ *
11082
+ * @throws {@link Vectros.BadRequestError}
11083
+ * @throws {@link Vectros.TooManyRequestsError}
11084
+ *
11085
+ * @example
11086
+ * await client.scripts.updateScript({
11087
+ * id: "id",
11088
+ * body: {
11089
+ * name: "notify-assignee",
11090
+ * source: "export default function(input) { return { ok: true }; }"
11091
+ * }
11092
+ * })
11093
+ */
11094
+ updateScript(request, requestOptions) {
11095
+ return HttpResponsePromise.fromPromise(this.__updateScript(request, requestOptions));
11096
+ }
11097
+ async __updateScript(request, requestOptions) {
11098
+ const { id, body: _body } = request;
11099
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11100
+ const _headers = mergeHeaders(
11101
+ _authRequest.headers,
11102
+ this._options?.headers,
11103
+ requestOptions?.headers
11104
+ );
11105
+ const _response = await fetcher({
11106
+ url: url_exports.join(
11107
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11108
+ `v1/scripts/${url_exports.encodePathParam(id)}`
11109
+ ),
11110
+ method: "PUT",
11111
+ headers: _headers,
11112
+ contentType: "application/json",
11113
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11114
+ requestType: "json",
11115
+ body: _body,
11116
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11117
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11118
+ abortSignal: requestOptions?.abortSignal,
11119
+ fetchFn: this._options?.fetch,
11120
+ logging: this._options.logging
11121
+ });
11122
+ if (_response.ok) {
11123
+ return { data: void 0, rawResponse: _response.rawResponse };
11124
+ }
11125
+ if (_response.error.reason === "status-code") {
11126
+ switch (_response.error.statusCode) {
11127
+ case 400:
11128
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
11129
+ case 429:
11130
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11131
+ default:
11132
+ throw new VectrosError({
11133
+ statusCode: _response.error.statusCode,
11134
+ body: _response.error.body,
11135
+ rawResponse: _response.rawResponse
11136
+ });
11137
+ }
11138
+ }
11139
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/scripts/{id}");
11140
+ }
11141
+ /**
11142
+ * 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.
11143
+ *
11144
+ * @param {Vectros.DeleteScriptRequest} request
11145
+ * @param {ScriptsClient.RequestOptions} requestOptions - Request-specific configuration.
11146
+ *
11147
+ * @throws {@link Vectros.NotFoundError}
11148
+ * @throws {@link Vectros.TooManyRequestsError}
11149
+ *
11150
+ * @example
11151
+ * await client.scripts.deleteScript({
11152
+ * id: "id"
11153
+ * })
11154
+ */
11155
+ deleteScript(request, requestOptions) {
11156
+ return HttpResponsePromise.fromPromise(this.__deleteScript(request, requestOptions));
11157
+ }
11158
+ async __deleteScript(request, requestOptions) {
11159
+ const { id } = request;
11160
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11161
+ const _headers = mergeHeaders(
11162
+ _authRequest.headers,
11163
+ this._options?.headers,
11164
+ requestOptions?.headers
11165
+ );
11166
+ const _response = await fetcher({
11167
+ url: url_exports.join(
11168
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11169
+ `v1/scripts/${url_exports.encodePathParam(id)}`
11170
+ ),
11171
+ method: "DELETE",
11172
+ headers: _headers,
11173
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11174
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11175
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11176
+ abortSignal: requestOptions?.abortSignal,
11177
+ fetchFn: this._options?.fetch,
11178
+ logging: this._options.logging
11179
+ });
11180
+ if (_response.ok) {
11181
+ return { data: void 0, rawResponse: _response.rawResponse };
11182
+ }
11183
+ if (_response.error.reason === "status-code") {
11184
+ switch (_response.error.statusCode) {
11185
+ case 404:
11186
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
11187
+ case 429:
11188
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11189
+ default:
11190
+ throw new VectrosError({
11191
+ statusCode: _response.error.statusCode,
11192
+ body: _response.error.body,
11193
+ rawResponse: _response.rawResponse
11194
+ });
11195
+ }
11196
+ }
11197
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/scripts/{id}");
11198
+ }
11199
+ };
11200
+ var SearchClient = class {
11201
+ constructor(options) {
11202
+ this._options = normalizeClientOptionsWithAuth(options);
11203
+ }
11204
+ /**
11205
+ * 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.
11206
+ *
11207
+ * @param {Vectros.SearchRequest} request
11208
+ * @param {SearchClient.RequestOptions} requestOptions - Request-specific configuration.
11209
+ *
11210
+ * @throws {@link Vectros.BadRequestError}
11211
+ * @throws {@link Vectros.ForbiddenError}
11212
+ * @throws {@link Vectros.TooManyRequestsError}
11213
+ *
11214
+ * @example
11215
+ * await client.search.content({
11216
+ * query: "patient intake form diabetes"
11217
+ * })
11218
+ */
11219
+ content(request, requestOptions) {
11220
+ return HttpResponsePromise.fromPromise(this.__content(request, requestOptions));
11221
+ }
11222
+ async __content(request, requestOptions) {
11223
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11224
+ const _headers = mergeHeaders(
11225
+ _authRequest.headers,
11226
+ this._options?.headers,
11227
+ requestOptions?.headers
11228
+ );
11229
+ const _response = await fetcher({
11230
+ url: url_exports.join(
11231
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11232
+ "v1/search"
11233
+ ),
11234
+ method: "POST",
11235
+ headers: _headers,
11236
+ contentType: "application/json",
11237
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11238
+ requestType: "json",
11239
+ body: request,
11240
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11241
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11242
+ abortSignal: requestOptions?.abortSignal,
11243
+ fetchFn: this._options?.fetch,
11244
+ logging: this._options.logging
11245
+ });
11246
+ if (_response.ok) {
11247
+ return { data: _response.body, rawResponse: _response.rawResponse };
11248
+ }
11249
+ if (_response.error.reason === "status-code") {
11250
+ switch (_response.error.statusCode) {
11251
+ case 400:
11252
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
11253
+ case 403:
11254
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
11255
+ case 429:
11256
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11257
+ default:
11258
+ throw new VectrosError({
11259
+ statusCode: _response.error.statusCode,
11260
+ body: _response.error.body,
11261
+ rawResponse: _response.rawResponse
11262
+ });
11263
+ }
11264
+ }
11265
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/search");
11266
+ }
11267
+ };
11268
+ var TriggersClient = class {
11269
+ constructor(options) {
11270
+ this._options = normalizeClientOptionsWithAuth(options);
11271
+ }
11272
+ /**
11273
+ * 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.
11274
+ *
11275
+ * 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.
11276
+ *
11277
+ * `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.
11278
+ *
11279
+ * Scoped to your account and app context, derived from your token — never from input. Requires the `triggers:r` scope.
11280
+ *
11281
+ * @param {Vectros.ListTriggerFailuresRequest} request
11282
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11283
+ *
11284
+ * @example
11285
+ * await client.triggers.listTriggerFailures({
11286
+ * ruleId: "3b1c9f2e7a5d8410f6c2e9a1b7d4038f5e6a91c2d7f0b3a4586c9e2f1d0a7b3c",
11287
+ * category: "SCRIPT_ERROR",
11288
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
11289
+ * })
11290
+ */
11291
+ listTriggerFailures(request = {}, requestOptions) {
11292
+ return HttpResponsePromise.fromPromise(this.__listTriggerFailures(request, requestOptions));
11293
+ }
11294
+ async __listTriggerFailures(request = {}, requestOptions) {
11295
+ const { ruleId, category, retryable, from: from_, to, startFrom, limit } = request;
11296
+ const _queryParams = {
11297
+ ruleId,
11298
+ category,
11299
+ retryable,
11300
+ from: from_,
11301
+ to,
11302
+ startFrom,
11303
+ limit
11304
+ };
11305
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11306
+ const _headers = mergeHeaders(
11307
+ _authRequest.headers,
11308
+ this._options?.headers,
11309
+ requestOptions?.headers
11310
+ );
11311
+ const _response = await fetcher({
11312
+ url: url_exports.join(
11313
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11314
+ "v1/trigger-failures"
11315
+ ),
11316
+ method: "GET",
11317
+ headers: _headers,
11318
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
11319
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11320
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11321
+ abortSignal: requestOptions?.abortSignal,
11322
+ fetchFn: this._options?.fetch,
11323
+ logging: this._options.logging
11324
+ });
11325
+ if (_response.ok) {
11326
+ return { data: _response.body, rawResponse: _response.rawResponse };
11327
+ }
11328
+ if (_response.error.reason === "status-code") {
11329
+ throw new VectrosError({
11330
+ statusCode: _response.error.statusCode,
11331
+ body: _response.error.body,
11332
+ rawResponse: _response.rawResponse
11333
+ });
11334
+ }
11335
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/trigger-failures");
11336
+ }
11337
+ /**
11338
+ * 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.
11339
+ *
11340
+ * @param {Vectros.ListTriggersRequest} request
11341
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11342
+ *
11343
+ * @example
11344
+ * await client.triggers.listTriggers({
11345
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
11346
+ * })
11347
+ */
11348
+ listTriggers(request = {}, requestOptions) {
11349
+ return HttpResponsePromise.fromPromise(this.__listTriggers(request, requestOptions));
11350
+ }
11351
+ async __listTriggers(request = {}, requestOptions) {
11352
+ const { startFrom, limit } = request;
11353
+ const _queryParams = {
11354
+ startFrom,
11355
+ limit
11356
+ };
11357
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11358
+ const _headers = mergeHeaders(
11359
+ _authRequest.headers,
11360
+ this._options?.headers,
11361
+ requestOptions?.headers
11362
+ );
11363
+ const _response = await fetcher({
11364
+ url: url_exports.join(
11365
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11366
+ "v1/triggers"
11367
+ ),
11368
+ method: "GET",
11369
+ headers: _headers,
11370
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
11371
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11372
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11373
+ abortSignal: requestOptions?.abortSignal,
11374
+ fetchFn: this._options?.fetch,
11375
+ logging: this._options.logging
11376
+ });
11377
+ if (_response.ok) {
11378
+ return { data: _response.body, rawResponse: _response.rawResponse };
11379
+ }
11380
+ if (_response.error.reason === "status-code") {
11381
+ throw new VectrosError({
11382
+ statusCode: _response.error.statusCode,
11383
+ body: _response.error.body,
11384
+ rawResponse: _response.rawResponse
11385
+ });
11386
+ }
11387
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/triggers");
11388
+ }
11389
+ /**
11390
+ * 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).
11391
+ *
11392
+ * @param {Vectros.CreateTriggerRequest} request
11393
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11394
+ *
11395
+ * @throws {@link Vectros.BadRequestError}
11396
+ * @throws {@link Vectros.ForbiddenError}
11397
+ * @throws {@link Vectros.TooManyRequestsError}
11398
+ *
11399
+ * @example
11400
+ * await client.triggers.createTrigger({
11401
+ * body: {
11402
+ * name: "on-intake-create",
11403
+ * firingSource: {
11404
+ * schemaId: "sch_9f8e7d6c",
11405
+ * event: "CREATE"
11406
+ * },
11407
+ * scriptRef: {
11408
+ * name: "notify-assignee",
11409
+ * version: "3"
11410
+ * }
11411
+ * }
11412
+ * })
11413
+ */
11414
+ createTrigger(request, requestOptions) {
11415
+ return HttpResponsePromise.fromPromise(this.__createTrigger(request, requestOptions));
11416
+ }
11417
+ async __createTrigger(request, requestOptions) {
11418
+ const { upsert, body: _body } = request;
11419
+ const _queryParams = {
11420
+ upsert
11421
+ };
11422
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11423
+ const _headers = mergeHeaders(
11424
+ _authRequest.headers,
11425
+ this._options?.headers,
11426
+ requestOptions?.headers
11427
+ );
11428
+ const _response = await fetcher({
11429
+ url: url_exports.join(
11430
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11431
+ "v1/triggers"
11432
+ ),
11433
+ method: "POST",
11434
+ headers: _headers,
11435
+ contentType: "application/json",
11436
+ queryString: url_exports.queryBuilder().addMany(_queryParams).mergeAdditional(requestOptions?.queryParams).build(),
11437
+ requestType: "json",
11438
+ body: _body,
11439
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11440
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11441
+ abortSignal: requestOptions?.abortSignal,
11442
+ fetchFn: this._options?.fetch,
11443
+ logging: this._options.logging
11444
+ });
11445
+ if (_response.ok) {
11446
+ return { data: _response.body, rawResponse: _response.rawResponse };
11447
+ }
11448
+ if (_response.error.reason === "status-code") {
11449
+ switch (_response.error.statusCode) {
11450
+ case 400:
11451
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
11452
+ case 403:
11453
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
11454
+ case 429:
11455
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11456
+ default:
11457
+ throw new VectrosError({
11458
+ statusCode: _response.error.statusCode,
11459
+ body: _response.error.body,
11460
+ rawResponse: _response.rawResponse
11461
+ });
11462
+ }
11463
+ }
11464
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/triggers");
11465
+ }
11466
+ /**
11467
+ * Retrieves a single trigger rule by its ID. Requires the `triggers:r` scope.
11468
+ *
11469
+ * @param {Vectros.GetTriggerRequest} request
11470
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11471
+ *
11472
+ * @throws {@link Vectros.NotFoundError}
11473
+ *
11474
+ * @example
11475
+ * await client.triggers.getTrigger({
11476
+ * id: "id"
11477
+ * })
11478
+ */
11479
+ getTrigger(request, requestOptions) {
11480
+ return HttpResponsePromise.fromPromise(this.__getTrigger(request, requestOptions));
11481
+ }
11482
+ async __getTrigger(request, requestOptions) {
11483
+ const { id } = request;
11484
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11485
+ const _headers = mergeHeaders(
11486
+ _authRequest.headers,
11487
+ this._options?.headers,
11488
+ requestOptions?.headers
11489
+ );
11490
+ const _response = await fetcher({
11491
+ url: url_exports.join(
11492
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11493
+ `v1/triggers/${url_exports.encodePathParam(id)}`
11494
+ ),
11495
+ method: "GET",
11496
+ headers: _headers,
11497
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11498
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11499
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11500
+ abortSignal: requestOptions?.abortSignal,
11501
+ fetchFn: this._options?.fetch,
11502
+ logging: this._options.logging
11503
+ });
11504
+ if (_response.ok) {
11505
+ return { data: _response.body, rawResponse: _response.rawResponse };
11506
+ }
11507
+ if (_response.error.reason === "status-code") {
11508
+ switch (_response.error.statusCode) {
11509
+ case 404:
11510
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
11511
+ default:
11512
+ throw new VectrosError({
11513
+ statusCode: _response.error.statusCode,
11514
+ body: _response.error.body,
11515
+ rawResponse: _response.rawResponse
11516
+ });
11517
+ }
11518
+ }
11519
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/triggers/{id}");
11520
+ }
11521
+ /**
11522
+ * 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.
11523
+ *
11524
+ * @param {Vectros.UpdateTriggerRequest} request
11525
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11526
+ *
11527
+ * @throws {@link Vectros.BadRequestError}
11528
+ * @throws {@link Vectros.ForbiddenError}
11529
+ * @throws {@link Vectros.NotFoundError}
11530
+ * @throws {@link Vectros.TooManyRequestsError}
11531
+ *
11532
+ * @example
11533
+ * await client.triggers.updateTrigger({
11534
+ * id: "id",
11535
+ * body: {
11536
+ * name: "on-intake-create",
11537
+ * firingSource: {
11538
+ * schemaId: "sch_9f8e7d6c",
11539
+ * event: "CREATE"
11540
+ * },
11541
+ * scriptRef: {
11542
+ * name: "notify-assignee",
11543
+ * version: "3"
11544
+ * }
11545
+ * }
11546
+ * })
11547
+ */
11548
+ updateTrigger(request, requestOptions) {
11549
+ return HttpResponsePromise.fromPromise(this.__updateTrigger(request, requestOptions));
11550
+ }
11551
+ async __updateTrigger(request, requestOptions) {
11552
+ const { id, body: _body } = request;
11553
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11554
+ const _headers = mergeHeaders(
11555
+ _authRequest.headers,
11556
+ this._options?.headers,
11557
+ requestOptions?.headers
11558
+ );
11559
+ const _response = await fetcher({
11560
+ url: url_exports.join(
11561
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11562
+ `v1/triggers/${url_exports.encodePathParam(id)}`
11563
+ ),
11564
+ method: "PUT",
11565
+ headers: _headers,
11566
+ contentType: "application/json",
11567
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11568
+ requestType: "json",
11569
+ body: _body,
11570
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11571
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11572
+ abortSignal: requestOptions?.abortSignal,
11573
+ fetchFn: this._options?.fetch,
11574
+ logging: this._options.logging
11575
+ });
11576
+ if (_response.ok) {
11577
+ return { data: _response.body, rawResponse: _response.rawResponse };
11578
+ }
11579
+ if (_response.error.reason === "status-code") {
11580
+ switch (_response.error.statusCode) {
11581
+ case 400:
11582
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
11583
+ case 403:
11584
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
11585
+ case 404:
11586
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
11587
+ case 429:
11588
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11589
+ default:
11590
+ throw new VectrosError({
11591
+ statusCode: _response.error.statusCode,
11592
+ body: _response.error.body,
11593
+ rawResponse: _response.rawResponse
11594
+ });
11595
+ }
11596
+ }
11597
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/triggers/{id}");
11598
+ }
11599
+ /**
11600
+ * Permanently deletes one trigger rule by ID. Requires the `triggers:d` scope.
11601
+ *
11602
+ * @param {Vectros.DeleteTriggerRequest} request
11603
+ * @param {TriggersClient.RequestOptions} requestOptions - Request-specific configuration.
11604
+ *
11605
+ * @throws {@link Vectros.NotFoundError}
11606
+ * @throws {@link Vectros.TooManyRequestsError}
11607
+ *
11608
+ * @example
11609
+ * await client.triggers.deleteTrigger({
11610
+ * id: "id"
11611
+ * })
11612
+ */
11613
+ deleteTrigger(request, requestOptions) {
11614
+ return HttpResponsePromise.fromPromise(this.__deleteTrigger(request, requestOptions));
11615
+ }
11616
+ async __deleteTrigger(request, requestOptions) {
11617
+ const { id } = request;
11618
+ const _authRequest = await this._options.authProvider.getAuthRequest();
11619
+ const _headers = mergeHeaders(
11620
+ _authRequest.headers,
11621
+ this._options?.headers,
11622
+ requestOptions?.headers
11623
+ );
11624
+ const _response = await fetcher({
11625
+ url: url_exports.join(
11626
+ await Supplier.get(this._options.baseUrl) ?? await Supplier.get(this._options.environment),
11627
+ `v1/triggers/${url_exports.encodePathParam(id)}`
11628
+ ),
11629
+ method: "DELETE",
11630
+ headers: _headers,
11631
+ queryString: url_exports.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(),
11632
+ timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1e3,
11633
+ maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
11634
+ abortSignal: requestOptions?.abortSignal,
11635
+ fetchFn: this._options?.fetch,
11636
+ logging: this._options.logging
11637
+ });
11638
+ if (_response.ok) {
11639
+ return { data: void 0, rawResponse: _response.rawResponse };
11640
+ }
11641
+ if (_response.error.reason === "status-code") {
11642
+ switch (_response.error.statusCode) {
11643
+ case 404:
11644
+ throw new NotFoundError(_response.error.body, _response.rawResponse);
11645
+ case 429:
11646
+ throw new TooManyRequestsError(_response.error.body, _response.rawResponse);
11647
+ default:
11648
+ throw new VectrosError({
11649
+ statusCode: _response.error.statusCode,
11650
+ body: _response.error.body,
11651
+ rawResponse: _response.rawResponse
11652
+ });
11653
+ }
11654
+ }
11655
+ return handleNonStatusCodeError(_response.error, _response.rawResponse, "DELETE", "/v1/triggers/{id}");
11656
+ }
11657
+ };
11658
+ var VectrosClient = class {
11659
+ constructor(options) {
11660
+ this._options = normalizeClientOptionsWithAuth(options);
11661
+ }
11662
+ get auth() {
11663
+ return this._auth ?? (this._auth = new AuthClient(this._options));
11664
+ }
11665
+ get documents() {
11666
+ return this._documents ?? (this._documents = new DocumentsClient(this._options));
11667
+ }
11668
+ get identity() {
11669
+ return this._identity ?? (this._identity = new IdentityClient(this._options));
11670
+ }
11671
+ get compliance() {
11672
+ return this._compliance ?? (this._compliance = new ComplianceClient(this._options));
11673
+ }
11674
+ get folders() {
11675
+ return this._folders ?? (this._folders = new FoldersClient(this._options));
11676
+ }
11677
+ get inference() {
11678
+ return this._inference ?? (this._inference = new InferenceClient(this._options));
11679
+ }
11680
+ get records() {
11681
+ return this._records ?? (this._records = new RecordsClient(this._options));
11682
+ }
11683
+ get schemas() {
11684
+ return this._schemas ?? (this._schemas = new SchemasClient(this._options));
11685
+ }
11686
+ get scripts() {
11687
+ return this._scripts ?? (this._scripts = new ScriptsClient(this._options));
11688
+ }
11689
+ get search() {
11690
+ return this._search ?? (this._search = new SearchClient(this._options));
11691
+ }
11692
+ get triggers() {
11693
+ return this._triggers ?? (this._triggers = new TriggersClient(this._options));
11694
+ }
11695
+ /**
11696
+ * Make a passthrough request using the SDK's configured auth, retry, logging, etc.
11697
+ * This is useful for making requests to endpoints not yet supported in the SDK.
11698
+ * The input can be a URL string, URL object, or Request object. Relative paths are resolved against the configured base URL.
11699
+ *
11700
+ * @param {Request | string | URL} input - The URL, path, or Request object.
11701
+ * @param {RequestInit} init - Standard fetch RequestInit options.
11702
+ * @param {core.PassthroughRequest.RequestOptions} requestOptions - Per-request overrides (timeout, retries, headers, abort signal).
11703
+ * @returns {Promise<Response>} A standard Response object.
11704
+ */
11705
+ async fetch(input, init, requestOptions) {
11706
+ return makePassthroughRequest(
11707
+ input,
11708
+ init,
11709
+ {
11710
+ baseUrl: this._options.baseUrl ?? this._options.environment,
11711
+ headers: this._options.headers,
11712
+ timeoutInSeconds: this._options.timeoutInSeconds,
11713
+ maxRetries: this._options.maxRetries,
11714
+ fetch: this._options.fetch,
11715
+ logging: this._options.logging,
11716
+ getAuthHeaders: async () => (await this._options.authProvider.getAuthRequest()).headers
11717
+ },
11718
+ requestOptions
11719
+ );
11720
+ }
11721
+ };
11722
+ var logging;
11723
+ ((logging2) => {
11724
+ logging2.LogLevel = LogLevel;
11725
+ logging2.ConsoleLogger = ConsoleLogger;
10831
11726
  })(logging || (logging = {}));
10832
11727
 
10833
11728
  // ../blueprints/dist/index.mjs
@@ -10879,6 +11774,20 @@ var BlueprintFieldDefSchema = import_zod.z.object({
10879
11774
  // the `s` reveal scope for this record type (SchemaRequest.FieldDef.sensitive).
10880
11775
  // Format passthrough — the loader forwards it to createSchema. Default false.
10881
11776
  sensitive: import_zod.z.boolean().optional(),
11777
+ // Keep this field on the RECORD ROW when the payload is stored out of line (a large record, or a
11778
+ // LARGE_PAYLOAD storage profile), so it appears in list/lookup projections without
11779
+ // `includePayload`.
11780
+ //
11781
+ // Load-bearing for triggers: a trigger rule's `fields` may name a field only if it is `inline`,
11782
+ // `filterable`, or a lookup field — and never `sensitive`. So this is the flag that makes an
11783
+ // otherwise-ordinary field projectable into a triggered script's `input.record`. Mutually
11784
+ // exclusive with `sensitive` (the platform refuses the combination). The platform also refuses,
11785
+ // with a 409, removing whatever keeps a field projectable while a live trigger rule still
11786
+ // projects it — dropping `inline` OR `filterable` OR the lookup that carried it, not just this
11787
+ // flag.
11788
+ //
11789
+ // Format passthrough — the loader forwards it to createSchema. Default false.
11790
+ inline: import_zod.z.boolean().optional(),
10882
11791
  // Reference-field surface — a typed foreign-key link to another record. The
10883
11792
  // platform (SchemaRequest.FieldDef) requires BOTH targetTypeName AND
10884
11793
  // targetSurface on a reference field; the blueprint format uses the SAME names
@@ -10901,6 +11810,13 @@ var BlueprintFieldDefSchema = import_zod.z.object({
10901
11810
  targetSurface: import_zod.z.string().min(1).optional(),
10902
11811
  cardinality: import_zod.z.enum(["one", "many"]).optional()
10903
11812
  }).strict().superRefine((field, ctx) => {
11813
+ if (field.inline === true && field.sensitive === true) {
11814
+ ctx.addIssue({
11815
+ code: import_zod.z.ZodIssueCode.custom,
11816
+ path: ["inline"],
11817
+ 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"
11818
+ });
11819
+ }
10904
11820
  const isReference = field.fieldType === "reference";
10905
11821
  const hasRefKeys = field.targetTypeName !== void 0 || field.targetField !== void 0 || field.targetSurface !== void 0 || field.cardinality !== void 0;
10906
11822
  if (isReference && field.targetTypeName === void 0) {
@@ -10994,7 +11910,8 @@ var BlueprintLookupFieldSchema = import_zod.z.union([
10994
11910
  })
10995
11911
  ]);
10996
11912
  var BlueprintCapabilitiesSchema = import_zod.z.object({
10997
- auditHistory: import_zod.z.boolean().optional()
11913
+ auditHistory: import_zod.z.boolean().optional(),
11914
+ triggersEnabled: import_zod.z.boolean().optional()
10998
11915
  }).strict();
10999
11916
  var BlueprintSchemaSchema = import_zod.z.object({
11000
11917
  typeName: import_zod.z.string().min(1),
@@ -11102,6 +12019,30 @@ var GrantedCapabilitiesSchema = import_zod.z.array(import_zod.z.string().min(1))
11102
12019
  seen.add(name);
11103
12020
  });
11104
12021
  });
12022
+ var MAX_ASSIGNABLE_ROLES = 20;
12023
+ var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
12024
+ var AssignableRolesSchema = import_zod.z.array(
12025
+ import_zod.z.string().regex(ROLE_ID_RE, {
12026
+ message: "an assignableRoles entry must be a roleId: 3-31 chars, a lowercase letter first, then lowercase letters/digits/dashes (e.g. 'case-handler')"
12027
+ })
12028
+ ).min(1, {
12029
+ message: "assignableRoles must either be omitted (no roleId restriction) or name at least one roleId \u2014 an empty list states no rule"
12030
+ }).max(MAX_ASSIGNABLE_ROLES, {
12031
+ message: `assignableRoles accepts at most ${MAX_ASSIGNABLE_ROLES} entries`
12032
+ }).optional().superRefine((roleIds, ctx) => {
12033
+ if (!roleIds) return;
12034
+ const seen = /* @__PURE__ */ new Set();
12035
+ roleIds.forEach((rid, i) => {
12036
+ if (seen.has(rid)) {
12037
+ ctx.addIssue({
12038
+ code: import_zod.z.ZodIssueCode.custom,
12039
+ path: [i],
12040
+ message: `assignableRoles names '${rid}' more than once \u2014 list it once`
12041
+ });
12042
+ }
12043
+ seen.add(rid);
12044
+ });
12045
+ });
11105
12046
  var BlueprintAccessProfileSchema = import_zod.z.object({
11106
12047
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
11107
12048
  // what enforces the data-plane-only security boundary.
@@ -11171,7 +12112,14 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
11171
12112
  // blast-radius question from granting the same capability to a role that
11172
12113
  // a person is bound to. The gate should weigh the two separately rather
11173
12114
  // than reuse one policy for both.
11174
- capabilities: GrantedCapabilitiesSchema
12115
+ capabilities: GrantedCapabilitiesSchema,
12116
+ // Optional roleId allow-list on this profile's own inline clause — see
12117
+ // {@link AssignableRolesSchema}. Allowed here for the same symmetry reason `capabilities` is:
12118
+ // the platform models an access profile's scopes as a list of the SAME ScopeClause a role clause
12119
+ // uses, and the loader already builds this profile as one such clause. Only meaningful when the
12120
+ // clause grants role-composing authority in the first place (`profiles:c`-shaped); on a
12121
+ // clause that grants none it is inert rather than an error, exactly as on the platform.
12122
+ assignableRoles: AssignableRolesSchema
11175
12123
  }).strict().superRefine((ap, ctx) => {
11176
12124
  const hasInline = ap.allowedActions !== void 0;
11177
12125
  const hasRoleIds = ap.roleIds !== void 0;
@@ -11198,13 +12146,21 @@ var BlueprintAccessProfileSchema = import_zod.z.object({
11198
12146
  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"
11199
12147
  });
11200
12148
  }
12149
+ if (ap.assignableRoles !== void 0) {
12150
+ ctx.addIssue({
12151
+ code: import_zod.z.ZodIssueCode.custom,
12152
+ path: ["assignableRoles"],
12153
+ 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"
12154
+ });
12155
+ }
11201
12156
  }
11202
12157
  });
11203
12158
  var BlueprintRoleClauseSchema = import_zod.z.object({
11204
12159
  allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1),
11205
12160
  dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
11206
12161
  dataScopeRef: import_zod.z.string().min(1).optional(),
11207
- capabilities: GrantedCapabilitiesSchema
12162
+ capabilities: GrantedCapabilitiesSchema,
12163
+ assignableRoles: AssignableRolesSchema
11208
12164
  }).strict().superRefine((c, ctx) => {
11209
12165
  if (c.dataScope !== void 0 && c.dataScopeRef !== void 0) {
11210
12166
  ctx.addIssue({
@@ -11220,6 +12176,115 @@ var BlueprintFragmentsSchema = import_zod.z.record(
11220
12176
  var BlueprintRolesSchema = import_zod.z.record(import_zod.z.array(BlueprintRoleClauseSchema).min(1));
11221
12177
  var BlueprintRoleAssumableSchema = import_zod.z.record(import_zod.z.array(import_zod.z.string().min(1)).min(1));
11222
12178
  var BlueprintRoleAssumableMapSchema = import_zod.z.record(BlueprintRoleAssumableSchema);
12179
+ var BlueprintTriggerFiringSourceSchema = import_zod.z.object({
12180
+ schemaName: import_zod.z.string().min(1),
12181
+ event: import_zod.z.enum(["CREATE", "UPDATE", "DELETE"])
12182
+ }).strict();
12183
+ var SCRIPT_VERSION_INTEGER_RE = /^[1-9][0-9]*$/;
12184
+ var BlueprintTriggerScriptRefSchema = import_zod.z.object({
12185
+ name: import_zod.z.string().min(1),
12186
+ version: import_zod.z.string().min(1)
12187
+ }).strict().superRefine((ref, ctx) => {
12188
+ if (ref.version !== "latest" && !SCRIPT_VERSION_INTEGER_RE.test(ref.version)) {
12189
+ ctx.addIssue({
12190
+ code: import_zod.z.ZodIssueCode.custom,
12191
+ path: ["version"],
12192
+ message: `scriptRef.version '${ref.version}' is invalid \u2014 must be a positive decimal integer (as a string, e.g. '3') or the literal 'latest'`
12193
+ });
12194
+ }
12195
+ });
12196
+ var MANIFEST_VERB_RE = /^[a-z]+(?:\.[a-z]+)+$/;
12197
+ var BlueprintTriggerManifestSchema = import_zod.z.array(
12198
+ import_zod.z.string().min(1).regex(MANIFEST_VERB_RE, {
12199
+ message: "a manifest entry must be '<noun>.<verb>' (e.g. 'records.update')"
12200
+ })
12201
+ ).optional();
12202
+ var BlueprintTriggerEntrySchema = import_zod.z.object({
12203
+ firingSource: BlueprintTriggerFiringSourceSchema,
12204
+ scriptRef: BlueprintTriggerScriptRefSchema,
12205
+ allowedActions: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
12206
+ roleIds: import_zod.z.array(import_zod.z.string().min(1)).min(1).optional(),
12207
+ dataScope: import_zod.z.record(import_zod.z.array(import_zod.z.union([import_zod.z.string().min(1), import_zod.z.null()]))).optional(),
12208
+ capabilities: GrantedCapabilitiesSchema,
12209
+ manifest: BlueprintTriggerManifestSchema,
12210
+ /**
12211
+ * REQUIRED. The record fields this rule projects into its script's `input.record` — and, on an
12212
+ * UPDATE firing, `input.previous` (the same fields as they were before the write). On a DELETE
12213
+ * firing `input.record` carries these fields from the deleted row, which is the only way a
12214
+ * script can see anything of it.
12215
+ *
12216
+ * **An empty list is the "I project nothing" declaration**, not an omission: the script still
12217
+ * receives `input.recordId`. It is required rather than defaulted precisely so that projecting
12218
+ * nothing is something an author STATES, not something they get by forgetting the field.
12219
+ *
12220
+ * Every entry must name a field the schema keeps INLINE — declared `inline: true`, or
12221
+ * `filterable`, or used as a lookup field (a leg of one, or its `sortBy` target) — and never one
12222
+ * marked `sensitive`.
12223
+ * `lintTriggerFields` checks that against the schema this blueprint declares; the platform
12224
+ * checks it again, and refuses a schema update that would un-inline a field a live rule still
12225
+ * projects.
12226
+ *
12227
+ * A field the record does not carry arrives as `undefined`, never null. `record` and `previous`
12228
+ * together are capped at 224 KB serialised — a firing whose projected input exceeds that is not
12229
+ * run, and is recorded as an `INPUT_TOO_LARGE` failure naming the size.
12230
+ */
12231
+ fields: import_zod.z.array(import_zod.z.string().min(1))
12232
+ }).strict().superRefine((entry, ctx) => {
12233
+ const hasInline = entry.allowedActions !== void 0;
12234
+ const hasRoleIds = entry.roleIds !== void 0;
12235
+ if (hasInline === hasRoleIds) {
12236
+ ctx.addIssue({
12237
+ code: import_zod.z.ZodIssueCode.custom,
12238
+ path: ["roleIds"],
12239
+ 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)"
12240
+ });
12241
+ return;
12242
+ }
12243
+ if (hasRoleIds) {
12244
+ if (entry.dataScope !== void 0) {
12245
+ ctx.addIssue({
12246
+ code: import_zod.z.ZodIssueCode.custom,
12247
+ path: ["dataScope"],
12248
+ 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"
12249
+ });
12250
+ }
12251
+ if (entry.capabilities !== void 0) {
12252
+ ctx.addIssue({
12253
+ code: import_zod.z.ZodIssueCode.custom,
12254
+ path: ["capabilities"],
12255
+ 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"
12256
+ });
12257
+ }
12258
+ }
12259
+ });
12260
+ var BlueprintTriggersSchema = import_zod.z.record(BlueprintTriggerEntrySchema);
12261
+ var SCRIPT_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
12262
+ var SCRIPT_SOURCE_MAX_BYTES = 3e5;
12263
+ var SCRIPT_INPUT_CONTRACT_MAX_BYTES = 5e4;
12264
+ var utf8Bytes = (s) => new TextEncoder().encode(s).length;
12265
+ var BlueprintScriptEntrySchema = import_zod.z.object({
12266
+ /**
12267
+ * The script's source text, shipped verbatim.
12268
+ *
12269
+ * NOTHING PARSES THIS — not this package, and not the platform, which stores it as-is. A syntax
12270
+ * error is therefore invisible until the version first RUNS, where it surfaces as a
12271
+ * `SCRIPT_ERROR` from the trigger rule that dispatched it. Validation here is limited to what
12272
+ * can be checked without a JS parser: non-blank, and within the platform's byte cap.
12273
+ */
12274
+ source: import_zod.z.string().refine((s) => s.trim().length > 0, { message: "'source' must not be blank" }).refine((s) => utf8Bytes(s) <= SCRIPT_SOURCE_MAX_BYTES, {
12275
+ message: `'source' exceeds the platform's ${SCRIPT_SOURCE_MAX_BYTES.toLocaleString("en-US")}-byte cap (measured in UTF-8 bytes, not characters)`
12276
+ }),
12277
+ /**
12278
+ * Optional free-text description of the input shape this script expects.
12279
+ *
12280
+ * DESCRIPTIVE ONLY — neither this package nor the platform validates a firing's input against
12281
+ * it. It documents the contract for a human reading the blueprint; it does not enforce one.
12282
+ */
12283
+ declaredInputContract: import_zod.z.string().min(1).refine((s) => utf8Bytes(s) <= SCRIPT_INPUT_CONTRACT_MAX_BYTES, {
12284
+ message: `'declaredInputContract' exceeds the platform's ${SCRIPT_INPUT_CONTRACT_MAX_BYTES.toLocaleString("en-US")}-byte cap (measured in UTF-8 bytes, not characters)`
12285
+ }).optional()
12286
+ }).strict();
12287
+ var BlueprintScriptsSchema = import_zod.z.record(BlueprintScriptEntrySchema);
11223
12288
  var IDENTITY_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
11224
12289
  var FORBIDDEN_IDENTITY_NAMESPACES = /* @__PURE__ */ new Set(["record", "document", "entity", "user"]);
11225
12290
  var IdentityKindSchema = import_zod.z.string().min(1).superRefine((val, ctx) => {
@@ -11254,6 +12319,24 @@ var BlueprintServicePrincipalSchema = import_zod.z.object({
11254
12319
  displayName: import_zod.z.string().min(1)
11255
12320
  }).strict();
11256
12321
  var ISSUER_ID_RE = CONTEXT_ID_RE;
12322
+ var MAX_CAPTURED_CLAIMS = 20;
12323
+ var MAX_CAPTURED_CLAIM_NAME_LENGTH = 64;
12324
+ function assertNoDuplicateClaimNames(field) {
12325
+ return (claims, ctx) => {
12326
+ if (!claims) return;
12327
+ const seen = /* @__PURE__ */ new Set();
12328
+ claims.forEach((name, i) => {
12329
+ if (seen.has(name)) {
12330
+ ctx.addIssue({
12331
+ code: import_zod.z.ZodIssueCode.custom,
12332
+ path: [i],
12333
+ message: `${field} repeats '${name}' \u2014 each claim name may appear only once`
12334
+ });
12335
+ }
12336
+ seen.add(name);
12337
+ });
12338
+ };
12339
+ }
11257
12340
  var BlueprintIssuerSchema = import_zod.z.object({
11258
12341
  /** Short slug identifying this issuer within your account. Immutable once registered. */
11259
12342
  issuerId: import_zod.z.string().regex(ISSUER_ID_RE, {
@@ -11308,7 +12391,36 @@ var BlueprintIssuerSchema = import_zod.z.object({
11308
12391
  message: "roleId must be 3-31 chars, start with a lowercase letter, then lowercase letters/digits/dashes (e.g. 'practitioner-member')"
11309
12392
  })
11310
12393
  }).strict()
11311
- ).optional()
12394
+ ).optional(),
12395
+ /**
12396
+ * Opt-in ADDITIONAL identity-claim capture: OIDC claim names — beyond `emailClaim`, which keeps
12397
+ * its own dedicated field — read from this issuer's tokens on every successful token exchange
12398
+ * and stored as your tenant's golden IdP-asserted identity copy for the signed-in user.
12399
+ *
12400
+ * Deliberately NOT a fixed set: name whatever claims your IdP actually asserts (standard ones
12401
+ * like `name`/`phone_number`, or your provider's own custom claims). Each name is read from the
12402
+ * verified token first, falling back to `userinfoUri` (when configured) only for names still
12403
+ * missing after that — so a claim your IdP puts only on the userinfo response still resolves,
12404
+ * provided you configured that endpoint.
12405
+ *
12406
+ * Omit entirely to capture nothing beyond email (the default, and the behaviour of every
12407
+ * registration authored before this field existed). Capped at 20 entries, each a non-blank name
12408
+ * of at most 64 characters; a duplicate is REJECTED rather than silently deduplicated, because a
12409
+ * repeated name is a sign the declaration does not mean what its author thinks.
12410
+ *
12411
+ * Pairs with the top-level `identityProjectionClaims`, which decides which of these captured
12412
+ * names are then projected onto access profiles in this context — capture and projection are
12413
+ * separate opt-ins, and capturing a claim does not by itself expose it on a profile.
12414
+ */
12415
+ capturedClaims: import_zod.z.array(
12416
+ import_zod.z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
12417
+ message: `a capturedClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
12418
+ }).refine((name) => name.trim().length > 0, {
12419
+ message: "a capturedClaims entry must be a non-blank OIDC claim name"
12420
+ })
12421
+ ).max(MAX_CAPTURED_CLAIMS, {
12422
+ message: `capturedClaims accepts at most ${MAX_CAPTURED_CLAIMS} entries`
12423
+ }).optional().superRefine(assertNoDuplicateClaimNames("capturedClaims"))
11312
12424
  }).strict();
11313
12425
  var BlueprintNamespaceSchema = import_zod.z.object({
11314
12426
  /** The namespace to register — same grammar as a `scope:<namespace>` key. */
@@ -11513,21 +12625,35 @@ var BlueprintSeedRecordSchema = import_zod.z.discriminatedUnion("surface", [
11513
12625
  RecordSeedSchema,
11514
12626
  DocumentSeedSchema
11515
12627
  ]);
11516
- var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
12628
+ var SELF_TOKEN_RE = /\$\{\{\s*(?:self|under|member|input)\.[^}]*?\s*\}\}/;
12629
+ var IDENTITY_DERIVED_TOKEN_RE = /\$\{\{\s*(?:self|under|member)\.[^}]*?\s*\}\}/;
12630
+ var INPUT_TOKEN_RE = /\$\{\{\s*input\.[^}]*?\s*\}\}/;
11517
12631
  function lintSelfTokenPlacement(value, ctx) {
11518
- const walk = (node, path4, inRoleDataScope) => {
12632
+ const walk = (node, path4, zone) => {
11519
12633
  if (typeof node === "string") {
11520
- if (!inRoleDataScope && SELF_TOKEN_RE.test(node)) {
12634
+ if (zone === "none" && SELF_TOKEN_RE.test(node)) {
12635
+ ctx.addIssue({
12636
+ code: import_zod.z.ZodIssueCode.custom,
12637
+ path: path4,
12638
+ 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)"
12639
+ });
12640
+ } else if (zone === "trigger" && IDENTITY_DERIVED_TOKEN_RE.test(node)) {
11521
12641
  ctx.addIssue({
11522
12642
  code: import_zod.z.ZodIssueCode.custom,
11523
12643
  path: path4,
11524
- message: "'${{ self.* }}' / '${{ member.* }}' is a runtime placeholder \u2014 it is only valid inside a roles[].dataScope value, a roleAssumable[] value, or a fragments[] value"
12644
+ 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."
12645
+ });
12646
+ } else if (zone === "self-family" && INPUT_TOKEN_RE.test(node)) {
12647
+ ctx.addIssue({
12648
+ code: import_zod.z.ZodIssueCode.custom,
12649
+ path: path4,
12650
+ 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."
11525
12651
  });
11526
12652
  }
11527
12653
  return;
11528
12654
  }
11529
12655
  if (Array.isArray(node)) {
11530
- node.forEach((v, i) => walk(v, [...path4, i], inRoleDataScope));
12656
+ node.forEach((v, i) => walk(v, [...path4, i], zone));
11531
12657
  return;
11532
12658
  }
11533
12659
  if (node && typeof node === "object") {
@@ -11535,12 +12661,13 @@ function lintSelfTokenPlacement(value, ctx) {
11535
12661
  const inRoleDataScopeClause = path4.length === 3 && path4[0] === "roles" && k === "dataScope";
11536
12662
  const inRoleAssumable = path4.length === 0 && k === "roleAssumable";
11537
12663
  const inFragments = path4.length === 0 && k === "fragments";
11538
- const entering = inRoleDataScope || inRoleDataScopeClause || inRoleAssumable || inFragments;
11539
- walk(v, [...path4, k], entering);
12664
+ const inTriggerDataScope = path4.length === 2 && path4[0] === "triggers" && k === "dataScope";
12665
+ const nextZone = zone === "trigger" || inTriggerDataScope ? "trigger" : zone === "self-family" || inRoleDataScopeClause || inRoleAssumable || inFragments ? "self-family" : "none";
12666
+ walk(v, [...path4, k], nextZone);
11540
12667
  }
11541
12668
  }
11542
12669
  };
11543
- walk(value, [], false);
12670
+ walk(value, [], "none");
11544
12671
  }
11545
12672
  var IDENTITY_REF_RE = /\$\{\{\s*identities\.([A-Za-z_]\w*)\s*\}\}/g;
11546
12673
  var IDENTITY_REF_LOOSE_RE = /\$\{\{\s*identities\.([^}]*?)\s*\}\}/g;
@@ -11694,6 +12821,44 @@ function lintDataScopeKey(key, path4, ctx) {
11694
12821
  });
11695
12822
  }
11696
12823
  }
12824
+ var RECORD_RESERVED_FILTERABLE_FIELD_IDS = /* @__PURE__ */ new Set([
12825
+ "tenantId",
12826
+ "owner_id",
12827
+ "folderId",
12828
+ "rootFolderId",
12829
+ "recordType"
12830
+ ]);
12831
+ var DOCUMENT_RESERVED_FILTERABLE_FIELD_IDS = /* @__PURE__ */ new Set([
12832
+ "tenantId",
12833
+ "model_type",
12834
+ "owner_id",
12835
+ "title",
12836
+ "folderId",
12837
+ "rootFolderId",
12838
+ "recordType"
12839
+ ]);
12840
+ function lintReservedFilterableFieldIds(value, ctx) {
12841
+ const bp = value;
12842
+ bp.schemas?.forEach((schema, si) => {
12843
+ const surfaces = schema.allowedSurfaces ?? ["record"];
12844
+ schema.fields?.forEach((field, fi) => {
12845
+ if (!field?.filterable || !field.fieldId) return;
12846
+ const collidesWith = /* @__PURE__ */ new Set();
12847
+ if (surfaces.includes("record") && RECORD_RESERVED_FILTERABLE_FIELD_IDS.has(field.fieldId)) {
12848
+ for (const k of RECORD_RESERVED_FILTERABLE_FIELD_IDS) collidesWith.add(k);
12849
+ }
12850
+ if (surfaces.includes("document") && DOCUMENT_RESERVED_FILTERABLE_FIELD_IDS.has(field.fieldId)) {
12851
+ for (const k of DOCUMENT_RESERVED_FILTERABLE_FIELD_IDS) collidesWith.add(k);
12852
+ }
12853
+ if (collidesWith.size === 0) return;
12854
+ ctx.addIssue({
12855
+ code: import_zod.z.ZodIssueCode.custom,
12856
+ path: ["schemas", si, "fields", fi, "filterable"],
12857
+ 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.`
12858
+ });
12859
+ });
12860
+ });
12861
+ }
11697
12862
  function lintDataScopeKeys(value, ctx) {
11698
12863
  const bp = value;
11699
12864
  for (const key of Object.keys(bp.accessProfile?.dataScope ?? {})) {
@@ -11711,6 +12876,11 @@ function lintDataScopeKeys(value, ctx) {
11711
12876
  lintDataScopeKey(key, ["fragments", name, key], ctx);
11712
12877
  }
11713
12878
  }
12879
+ for (const [name, trigger] of Object.entries(bp.triggers ?? {})) {
12880
+ for (const key of Object.keys(trigger.dataScope ?? {})) {
12881
+ lintDataScopeKey(key, ["triggers", name, "dataScope", key], ctx);
12882
+ }
12883
+ }
11714
12884
  }
11715
12885
  function lintExpectedScopeDims(value, ctx) {
11716
12886
  const bp = value;
@@ -11806,6 +12976,150 @@ function lintAccessProfileRoleIds(value, ctx) {
11806
12976
  seen.add(roleId);
11807
12977
  });
11808
12978
  }
12979
+ function lintTriggerFields(value, ctx) {
12980
+ const bp = value;
12981
+ for (const [triggerName, entry] of Object.entries(bp.triggers ?? {})) {
12982
+ const declared = entry?.fields;
12983
+ if (!declared?.length) continue;
12984
+ const schema = bp.schemas?.find((sc) => sc.typeName === entry.firingSource?.schemaName);
12985
+ if (!schema) continue;
12986
+ const lookupNames = /* @__PURE__ */ new Set();
12987
+ for (const lf of schema.lookupFields ?? []) {
12988
+ if (typeof lf === "string") {
12989
+ lookupNames.add(lf);
12990
+ continue;
12991
+ }
12992
+ if (lf?.fieldName) lookupNames.add(lf.fieldName);
12993
+ for (const n of lf?.fieldNames ?? []) lookupNames.add(n);
12994
+ if (lf?.sortBy && lf.sortBy !== "createdAt" && lf.sortBy !== "lastUpdated") {
12995
+ lookupNames.add(lf.sortBy);
12996
+ }
12997
+ }
12998
+ const seenFields = /* @__PURE__ */ new Set();
12999
+ declared.forEach((fieldId, i) => {
13000
+ const def = schema.fields?.find((f) => f.fieldId === fieldId);
13001
+ const path4 = ["triggers", triggerName, "fields", i];
13002
+ if (seenFields.has(fieldId)) {
13003
+ ctx.addIssue({
13004
+ code: import_zod.z.ZodIssueCode.custom,
13005
+ path: path4,
13006
+ message: `triggers['${triggerName}'].fields names '${fieldId}' more than once \u2014 list it once`
13007
+ });
13008
+ return;
13009
+ }
13010
+ seenFields.add(fieldId);
13011
+ if (def?.sensitive === true) {
13012
+ ctx.addIssue({
13013
+ code: import_zod.z.ZodIssueCode.custom,
13014
+ path: path4,
13015
+ 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`
13016
+ });
13017
+ return;
13018
+ }
13019
+ if (lookupNames.has(fieldId)) return;
13020
+ if (!def) {
13021
+ ctx.addIssue({
13022
+ code: import_zod.z.ZodIssueCode.custom,
13023
+ path: path4,
13024
+ message: `triggers['${triggerName}'].fields names '${fieldId}', which schema '${schema.typeName}' neither declares nor uses as a lookup`
13025
+ });
13026
+ return;
13027
+ }
13028
+ if (def.inline !== true && def.filterable !== true) {
13029
+ ctx.addIssue({
13030
+ code: import_zod.z.ZodIssueCode.custom,
13031
+ path: path4,
13032
+ 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'`
13033
+ });
13034
+ }
13035
+ });
13036
+ }
13037
+ }
13038
+ function lintScripts(value, ctx) {
13039
+ const bp = value;
13040
+ const scripts = bp.scripts;
13041
+ if (!scripts) return;
13042
+ for (const name of Object.keys(scripts)) {
13043
+ if (!SCRIPT_NAME_RE.test(name)) {
13044
+ ctx.addIssue({
13045
+ code: import_zod.z.ZodIssueCode.custom,
13046
+ path: ["scripts", name],
13047
+ 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.`
13048
+ });
13049
+ }
13050
+ }
13051
+ for (const [triggerName, trigger] of Object.entries(bp.triggers ?? {})) {
13052
+ const ref = trigger?.scriptRef;
13053
+ if (!ref?.name) continue;
13054
+ if (!Object.prototype.hasOwnProperty.call(scripts, ref.name)) continue;
13055
+ if (ref.version !== "latest") {
13056
+ ctx.addIssue({
13057
+ code: import_zod.z.ZodIssueCode.custom,
13058
+ path: ["triggers", triggerName, "scriptRef", "version"],
13059
+ 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.`
13060
+ });
13061
+ }
13062
+ }
13063
+ }
13064
+ function lintTriggers(value, ctx) {
13065
+ const bp = value;
13066
+ const triggers = bp.triggers;
13067
+ if (!triggers) return;
13068
+ const declaredSchemas = new Set((bp.schemas ?? []).map((s) => s.typeName).filter(Boolean));
13069
+ const triggerEnabledSchemas = new Set(
13070
+ (bp.schemas ?? []).filter((s) => s.capabilities?.triggersEnabled === true).map((s) => s.typeName).filter(Boolean)
13071
+ );
13072
+ const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
13073
+ for (const [name, trigger] of Object.entries(triggers)) {
13074
+ const schemaName = trigger.firingSource?.schemaName;
13075
+ if (schemaName && !declaredSchemas.has(schemaName)) {
13076
+ ctx.addIssue({
13077
+ code: import_zod.z.ZodIssueCode.custom,
13078
+ path: ["triggers", name, "firingSource", "schemaName"],
13079
+ 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)")
13080
+ });
13081
+ } else if (schemaName && !triggerEnabledSchemas.has(schemaName)) {
13082
+ ctx.addIssue({
13083
+ code: import_zod.z.ZodIssueCode.custom,
13084
+ path: ["triggers", name, "firingSource", "schemaName"],
13085
+ 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.`
13086
+ });
13087
+ }
13088
+ const roleIds = trigger.roleIds;
13089
+ if (!roleIds) continue;
13090
+ const seen = /* @__PURE__ */ new Set();
13091
+ roleIds.forEach((roleId, i) => {
13092
+ const path4 = ["triggers", name, "roleIds", i];
13093
+ if (!declaredRoles.has(roleId)) {
13094
+ ctx.addIssue({
13095
+ code: import_zod.z.ZodIssueCode.custom,
13096
+ path: path4,
13097
+ 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)")
13098
+ });
13099
+ }
13100
+ if (seen.has(roleId)) {
13101
+ ctx.addIssue({
13102
+ code: import_zod.z.ZodIssueCode.custom,
13103
+ path: path4,
13104
+ message: `triggers['${name}'].roleIds names '${roleId}' more than once \u2014 remove the duplicate`
13105
+ });
13106
+ }
13107
+ seen.add(roleId);
13108
+ for (const clause of bp.roles?.[roleId] ?? []) {
13109
+ for (const [dim, raw] of Object.entries(clause?.dataScope ?? {})) {
13110
+ for (const v of Array.isArray(raw) ? raw : [raw]) {
13111
+ if (typeof v !== "string" || !IDENTITY_DERIVED_TOKEN_RE.test(v)) continue;
13112
+ ctx.addIssue({
13113
+ code: import_zod.z.ZodIssueCode.custom,
13114
+ path: path4,
13115
+ 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.`
13116
+ });
13117
+ }
13118
+ }
13119
+ }
13120
+ });
13121
+ }
13122
+ }
11809
13123
  function lintRoleAssumable(value, ctx) {
11810
13124
  const bp = value;
11811
13125
  const declaredRoles = new Set(Object.keys(bp.roles ?? {}));
@@ -11920,7 +13234,21 @@ var BLUEPRINT_FIELD_PHASES = Object.freeze({
11920
13234
  schemas: "in-context",
11921
13235
  accessProfile: "in-context",
11922
13236
  roles: "in-context",
11923
- seed: "in-context"
13237
+ seed: "in-context",
13238
+ // Pushed on the same in-context token as the schemas/roles above, and — load-bearing — BEFORE
13239
+ // `triggers`, because a trigger's `scriptRef` is validated AT DECLARE TIME: a rule naming a script
13240
+ // with no versions in the context is refused with a 400. This map does not order the loader (a
13241
+ // plain object's key order is not a contract); the loader orders itself, and a test pins it.
13242
+ scripts: "in-context",
13243
+ // A trigger's principal + grant are provisioned exactly like the roles: block is (N service-
13244
+ // principal/AccessProfile pairs instead of one), so it applies in the identical in-context phase,
13245
+ // under the same re-minted context-confined token.
13246
+ triggers: "in-context",
13247
+ // Setting it requires the platform provisioning capability, which only the bootstrap credential
13248
+ // carries — the same reasoning as issuers/namespaces above. It is applied to the app context
13249
+ // itself, which the bootstrap phase already creates, so there is nothing for the in-context pass
13250
+ // to do with it.
13251
+ identityProjectionClaims: "bootstrap"
11924
13252
  });
11925
13253
  var BlueprintSchema = import_zod.z.object({
11926
13254
  /** Stable blueprint id (the `--blueprint <name>` selector + idempotency key). */
@@ -11983,13 +13311,77 @@ var BlueprintSchema = import_zod.z.object({
11983
13311
  * `allowedSurfaces: ['entity']` schema, both need the registration to
11984
13312
  * already exist). `org`/`client` need no entry — they exist already.
11985
13313
  */
11986
- namespaces: import_zod.z.array(BlueprintNamespaceSchema).optional()
13314
+ namespaces: import_zod.z.array(BlueprintNamespaceSchema).optional(),
13315
+ /**
13316
+ * Declares which of the golden IdP-asserted claim names captured by this tenant's issuers (see
13317
+ * `issuers[].capturedClaims`) are projected, read-only, onto access profiles in THIS context.
13318
+ * Applied to the app context in the BOOTSTRAP-token phase, alongside the context's own create.
13319
+ *
13320
+ * Capture and projection are deliberately separate opt-ins: capturing a claim stores it against
13321
+ * the user, projecting it exposes it on a profile in this context. Declaring a name here that no
13322
+ * issuer captures is not an error — it simply never fills.
13323
+ *
13324
+ * <p><b>Fills once, then freezes.</b> A profile's projection is written the first time a sign-in
13325
+ * for that principal can supply a value — usually at profile creation, but for an INVITED member
13326
+ * not until they actually accept and sign in, since there is nothing to project before that.
13327
+ * Once filled it never updates again, even if this declaration or the underlying identity data
13328
+ * changes later. So editing this list affects only profiles that have not been filled yet; it is
13329
+ * not a backfill, and re-applying a blueprint does not re-project existing profiles.
13330
+ *
13331
+ * <p><b>Authority.</b> Setting this needs the platform provisioning capability — the same
13332
+ * authority the bootstrap credential already uses to register issuers and declare namespaces.
13333
+ * An ordinary `app-contexts:u` credential may rename its own context but may not opt it into
13334
+ * projecting IdP-golden identity data, so this field is only settable on the bootstrap phase's
13335
+ * own token, never on the per-context wildcard the in-context load runs under.
13336
+ *
13337
+ * <p>Omit to leave the context's existing declaration unchanged; declare an empty list to
13338
+ * disable future projection. Capped at 20 entries, each a non-blank name of at most 64
13339
+ * characters, duplicates rejected — the same bounds `capturedClaims` carries.
13340
+ *
13341
+ * <p><b>Applies only when this blueprint CREATES the app context.</b> The condition is the
13342
+ * context being created, not the apply being the first one: creating the context separately
13343
+ * beforehand means even the first apply does not set it. Re-applying against a context that
13344
+ * already exists does not change it, in either direction. Edit the declaration on an existing context through the platform API instead. (A
13345
+ * tool applying this blueprint could in principle reconcile it, but doing so also overwrites the
13346
+ * context's name and needs a broader scope — see `@vectros-ai/cli`'s own notes.)
13347
+ */
13348
+ identityProjectionClaims: import_zod.z.array(
13349
+ import_zod.z.string().max(MAX_CAPTURED_CLAIM_NAME_LENGTH, {
13350
+ message: `an identityProjectionClaims entry must be at most ${MAX_CAPTURED_CLAIM_NAME_LENGTH} characters`
13351
+ }).refine((name) => name.trim().length > 0, {
13352
+ message: "an identityProjectionClaims entry must be a non-blank claim name"
13353
+ })
13354
+ ).max(MAX_CAPTURED_CLAIMS, {
13355
+ message: `identityProjectionClaims accepts at most ${MAX_CAPTURED_CLAIMS} entries`
13356
+ }).optional().superRefine(assertNoDuplicateClaimNames("identityProjectionClaims")),
13357
+ /**
13358
+ * Optional scripts this blueprint SHIPS: scriptName -> `{ source, declaredInputContract? }`.
13359
+ *
13360
+ * Distinct from a trigger's `scriptRef`, which only NAMES a script and requires it to already
13361
+ * exist. Pushed BEFORE the `triggers` block, because a `scriptRef` is validated at declare time.
13362
+ *
13363
+ * Each entry is the CURRENT SOURCE for that name, not a version — the platform assigns versions
13364
+ * itself and they are immutable. Re-applying an unchanged blueprint pushes nothing; editing
13365
+ * `source` pushes a new version, which every `version: 'latest'` rule then picks up.
13366
+ */
13367
+ scripts: BlueprintScriptsSchema.optional(),
13368
+ /**
13369
+ * Optional async trigger rules: triggerName -> declaration.
13370
+ *
13371
+ * These FIRE. A record write on a schema that opts in via `capabilities.triggersEnabled`
13372
+ * dispatches the rule's script, which runs asynchronously in a sandbox under the grant declared
13373
+ * here — so a trigger's `allowedActions`/`dataScope`/`capabilities` (or the roles its `roleIds`
13374
+ * composes) are live authority, not a declaration awaiting a future release. Author the grant as
13375
+ * narrowly as you would any other credential's.
13376
+ */
13377
+ triggers: BlueprintTriggersSchema.optional()
11987
13378
  }).strict().superRefine((bp, ctx) => {
11988
13379
  lintSelfTokenPlacement(bp, ctx);
11989
13380
  lintIdentityRefs(bp, ctx);
11990
13381
  lintIdentityOverrides(bp, ctx);
11991
13382
  lintNamespacedScopeArrays(bp, ctx);
11992
13383
  lintDataScopeKeys(bp, ctx);
13384
+ lintReservedFilterableFieldIds(bp, ctx);
11993
13385
  lintExpectedScopeDims(bp, ctx);
11994
13386
  lintDataScopeRefs(bp, ctx);
11995
13387
  lintRoleAssumable(bp, ctx);
@@ -11998,6 +13390,9 @@ var BlueprintSchema = import_zod.z.object({
11998
13390
  lintIdentitySurfaces(bp, ctx);
11999
13391
  lintIssuerContext(bp, ctx);
12000
13392
  lintNamespaces(bp, ctx);
13393
+ lintTriggers(bp, ctx);
13394
+ lintTriggerFields(bp, ctx);
13395
+ lintScripts(bp, ctx);
12001
13396
  });
12002
13397
  function formatIssuePath(path4) {
12003
13398
  let out = "";
@@ -14082,6 +15477,13 @@ function renderActionableError(info) {
14082
15477
  return ` \u2716 ${msg}
14083
15478
  Your credential lacks the scope for this operation (owner-gated, or a
14084
15479
  narrower context/action than required).
15480
+ `;
15481
+ }
15482
+ if (info.statusCode === 409 && /no scoped key can be minted|is not active|is suspended/i.test(msg)) {
15483
+ return ` \u2716 ${msg}
15484
+ This is NOT a scope problem: no credential, including a root key, can mint against an
15485
+ inactive profile or a suspended user, so re-issuing a broader credential will not help.
15486
+ Follow the reactivation step named above, then retry.
14085
15487
  `;
14086
15488
  }
14087
15489
  if (info.statusCode === 404) {
@@ -14248,7 +15650,10 @@ async function mintBridgeToken(opts) {
14248
15650
  let url = `${base}${path4}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}`;
14249
15651
  if (contextId) url += `&context=${encodeURIComponent(contextId)}`;
14250
15652
  return fetchStToken(url, cognitoToken, fetchImpl, path4, (status) => {
14251
- if (status !== 404 || useScopedToken || contextId) return "";
15653
+ if (status !== 404) return "";
15654
+ if (useScopedToken || contextId) {
15655
+ 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)";
15656
+ }
14252
15657
  return ` (the dedicated ${CLI_BOOTSTRAP_TOKEN_PATH} endpoint may not be deployed yet \u2014 retry with --use-scoped-token for the fallback path)`;
14253
15658
  });
14254
15659
  }
@@ -14263,7 +15668,10 @@ async function mintContextToken(opts) {
14263
15668
  } = opts;
14264
15669
  const base = apiBaseUrl.replace(/\/$/, "");
14265
15670
  const url = `${base}${SCOPED_TOKEN_PATH}?ttl=${encodeURIComponent(String(ttlSeconds))}&tenant=${encodeURIComponent(tenant)}&context=${encodeURIComponent(contextId)}`;
14266
- return fetchStToken(url, cognitoToken, fetchImpl, `${SCOPED_TOKEN_PATH}?context=${contextId}`);
15671
+ return fetchStToken(url, cognitoToken, fetchImpl, `${SCOPED_TOKEN_PATH}?context=${contextId}`, (status) => {
15672
+ if (status !== 404) return "";
15673
+ 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)";
15674
+ });
14267
15675
  }
14268
15676
 
14269
15677
  // src/identity.ts
@@ -14650,7 +16058,21 @@ var KNOWN_CONTROL_PLANE_RESOURCES = [
14650
16058
  // unconditionally, never tenant-wide, so the reach is bounded to what the
14651
16059
  // blueprint already provisions, never the pack's say-so alone. See
14652
16060
  // `@vectros-ai/blueprints`' `namespaces[]` doc for the full design.
14653
- "namespaces"
16061
+ "namespaces",
16062
+ // The 0.43.0 script/trigger surfaces. Listed for MESSAGE QUALITY only — both were already
16063
+ // rejected by the data-plane allowlist above, but as "unrecognized resource", whose text invites
16064
+ // the reader to widen the allowlist ("if 'scripts' is a new data-plane resource…"). They are not:
16065
+ // the platform deliberately put them out of reach of an ordinary confined partner role (a bare,
16066
+ // non-empty `data_scope` clause — the routine blueprint-issued shape — does not reach scripts at
16067
+ // all), because script authoring is a design-time, admin-shaped surface. So the accurate message
16068
+ // is the control-plane one.
16069
+ //
16070
+ // NOTE on the `x` action letter: `scripts:x` (execute) is a real platform verb, and VALID_OPS
16071
+ // deliberately does NOT gain it. Op letters are only checked AFTER a resource is admitted, and
16072
+ // no admitted resource has an `x` verb — adding it would instead make `records:x` pass the op
16073
+ // check and be admitted as data-plane.
16074
+ "scripts",
16075
+ "triggers"
14654
16076
  ];
14655
16077
  var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
14656
16078
  ["profiles", /* @__PURE__ */ new Set(["r", "u", "d"])]
@@ -14658,6 +16080,13 @@ var PARTIALLY_ADMITTED_OPS = /* @__PURE__ */ new Map([
14658
16080
  var SELF_QUALIFIED_OPS = /* @__PURE__ */ new Map([
14659
16081
  ["profiles", /* @__PURE__ */ new Set(["c"])]
14660
16082
  ]);
16083
+ var NAME_QUALIFIED_OPS = /* @__PURE__ */ new Map([
16084
+ ["scripts", /* @__PURE__ */ new Set(["x"])]
16085
+ ]);
16086
+ var RESOURCE_EXTRA_OPS = /* @__PURE__ */ new Map([
16087
+ ["scripts", /* @__PURE__ */ new Set(["x"])]
16088
+ ]);
16089
+ var PLATFORM_SCRIPT_NAME = /^[A-Za-z0-9_-]{1,64}$/;
14661
16090
  var UNQUALIFIABLE_WITH_ANY_TAIL = /* @__PURE__ */ new Map([
14662
16091
  ["profiles", /* @__PURE__ */ new Set(["r"])]
14663
16092
  ]);
@@ -14668,10 +16097,13 @@ var VALID_OPS = /* @__PURE__ */ new Set(["c", "r", "u", "d"]);
14668
16097
  var DATA_PLANE_SET = new Set(DATA_PLANE_ALLOWLIST);
14669
16098
  var CONTROL_PLANE_SET = new Set(KNOWN_CONTROL_PLANE_RESOURCES);
14670
16099
  var ALLOWLIST_HINT = DATA_PLANE_ALLOWLIST.join("/");
16100
+ function formatRejection(r) {
16101
+ return r.source ? `${r.action} (${r.source}): ${r.reason}` : `${r.action}: ${r.reason}`;
16102
+ }
14671
16103
  var ScopeGateError = class extends Error {
14672
16104
  rejected;
14673
16105
  constructor(rejected) {
14674
- const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
16106
+ const summary = rejected.map((r) => ` \u2022 ${formatRejection(r)}`).join("\n");
14675
16107
  super(
14676
16108
  `Scope gate rejected ${rejected.length} requested scope(s); the bootstrap loader provisions data-plane access only (${ALLOWLIST_HINT}) and mints nothing on reject:
14677
16109
  ${summary}`
@@ -14686,7 +16118,7 @@ var CAPABILITIES_HINT = GRANTED_CAPABILITIES_ALLOWLIST.join("/");
14686
16118
  var CapabilityGateError = class extends Error {
14687
16119
  rejected;
14688
16120
  constructor(rejected) {
14689
- const summary = rejected.map((r) => ` \u2022 ${r.action}: ${r.reason}`).join("\n");
16121
+ const summary = rejected.map((r) => ` \u2022 ${formatRejection(r)}`).join("\n");
14690
16122
  super(
14691
16123
  `Capability gate rejected ${rejected.length} requested capability(ies); the bootstrap loader grants (${CAPABILITIES_HINT}) only and mints nothing on reject:
14692
16124
  ${summary}`
@@ -14727,9 +16159,10 @@ function assertCapabilityGate(capabilities) {
14727
16159
  throw new CapabilityGateError(result.rejected);
14728
16160
  }
14729
16161
  }
14730
- function firstInvalidOp(ops) {
16162
+ function firstInvalidOp(ops, resource) {
16163
+ const extra = resource === void 0 ? void 0 : RESOURCE_EXTRA_OPS.get(resource);
14731
16164
  for (const ch of ops) {
14732
- if (!VALID_OPS.has(ch)) return ch;
16165
+ if (!VALID_OPS.has(ch) && !extra?.has(ch)) return ch;
14733
16166
  }
14734
16167
  return void 0;
14735
16168
  }
@@ -14756,19 +16189,24 @@ function evaluateScopeToken(action, capabilities) {
14756
16189
  if (!DATA_PLANE_SET.has(resource)) {
14757
16190
  const admittedOps = PARTIALLY_ADMITTED_OPS.get(resource);
14758
16191
  const selfOnlyOps = SELF_QUALIFIED_OPS.get(resource);
14759
- if (admittedOps || selfOnlyOps) {
14760
- const admittedHint = [.../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? []])].sort().join("/");
16192
+ const nameOnlyOps = NAME_QUALIFIED_OPS.get(resource);
16193
+ if (admittedOps || selfOnlyOps || nameOnlyOps) {
16194
+ const admittedHint = [
16195
+ .../* @__PURE__ */ new Set([...admittedOps ?? [], ...selfOnlyOps ?? [], ...nameOnlyOps ?? []])
16196
+ ].sort().join("/");
14761
16197
  if (ops === "*") {
14762
16198
  return {
14763
16199
  action,
14764
16200
  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`
14765
16201
  };
14766
16202
  }
14767
- const invalidOp2 = firstInvalidOp(ops);
16203
+ const invalidOp2 = firstInvalidOp(ops, resource);
14768
16204
  if (invalidOp2) {
16205
+ const extraOps = [...RESOURCE_EXTRA_OPS.get(resource) ?? []].sort();
16206
+ const validHint = ["c", "r", "u", "d", ...extraOps].join(", ");
14769
16207
  return {
14770
16208
  action,
14771
- reason: `invalid operation character '${invalidOp2}' (valid operations are c, r, u, d, or '*')`
16209
+ reason: `invalid operation character '${invalidOp2}' (valid operations are ${validHint}, or '*')`
14772
16210
  };
14773
16211
  }
14774
16212
  const qualifier = parts.length > 2 ? parts.slice(2).join(":") : void 0;
@@ -14778,10 +16216,29 @@ function evaluateScopeToken(action, capabilities) {
14778
16216
  if (qualifier !== void 0 && unqualifiableOps?.has(ch)) {
14779
16217
  return {
14780
16218
  action,
14781
- 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).`
16219
+ 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).`
16220
+ };
16221
+ }
16222
+ if (admittedOps?.has(ch)) continue;
16223
+ if (nameOnlyOps?.has(ch)) {
16224
+ if (qualifier !== void 0 && qualifier.includes("*")) {
16225
+ return {
16226
+ action,
16227
+ 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>'`
16228
+ };
16229
+ }
16230
+ if (qualifier !== void 0 && PLATFORM_SCRIPT_NAME.test(qualifier)) continue;
16231
+ if (qualifier !== void 0 && qualifier.length > 0) {
16232
+ return {
16233
+ action,
16234
+ 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`
16235
+ };
16236
+ }
16237
+ return {
16238
+ action,
16239
+ 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.`
14782
16240
  };
14783
16241
  }
14784
- if (admittedOps?.has(ch)) continue;
14785
16242
  if (selfOnlyOps?.has(ch)) {
14786
16243
  if (isSelfQualified) continue;
14787
16244
  const unlockCapability = CAPABILITY_UNLOCKED_OPS.get(resource)?.get(ch);
@@ -14852,6 +16309,50 @@ function assertBlueprintGates(blueprint) {
14852
16309
  assertCapabilityGate(clause.capabilities);
14853
16310
  }
14854
16311
  }
16312
+ for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
16313
+ if (entry.allowedActions === void 0) continue;
16314
+ try {
16315
+ assertScopeGate(entry.allowedActions, entry.capabilities);
16316
+ assertCapabilityGate(entry.capabilities);
16317
+ } catch (err) {
16318
+ throw withTriggerContext(err, triggerName);
16319
+ }
16320
+ }
16321
+ assertScriptGrantsNameADeclaredScript(blueprint);
16322
+ }
16323
+ function assertScriptGrantsNameADeclaredScript(blueprint) {
16324
+ const declared = Object.keys(blueprint.scripts ?? {});
16325
+ if (declared.length === 0) return;
16326
+ const rejected = [];
16327
+ const check = (actions, source) => {
16328
+ for (const action of actions ?? []) {
16329
+ const parts = action.trim().split(":");
16330
+ if (parts[0] !== "scripts" || parts[1] !== "x") continue;
16331
+ const name = parts.length > 2 ? parts.slice(2).join(":") : void 0;
16332
+ if (name === void 0 || name.length === 0 || name.includes("*")) continue;
16333
+ if (!declared.includes(name)) {
16334
+ rejected.push({
16335
+ action,
16336
+ 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`,
16337
+ ...source ? { source } : {}
16338
+ });
16339
+ }
16340
+ }
16341
+ };
16342
+ check(blueprint.accessProfile.allowedActions);
16343
+ for (const [roleId, clauses] of Object.entries(blueprint.roles ?? {})) {
16344
+ for (const clause of clauses) check(clause.allowedActions, `role '${roleId}'`);
16345
+ }
16346
+ for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
16347
+ check(entry.allowedActions, `trigger '${triggerName}'`);
16348
+ }
16349
+ if (rejected.length > 0) throw new ScopeGateError(rejected);
16350
+ }
16351
+ function withTriggerContext(err, triggerName) {
16352
+ if (err instanceof ScopeGateError || err instanceof CapabilityGateError) {
16353
+ for (const r of err.rejected) r.source ??= `trigger '${triggerName}'`;
16354
+ }
16355
+ return err;
14855
16356
  }
14856
16357
  function effectiveAccessProfileActions(blueprint) {
14857
16358
  if (blueprint.accessProfile.allowedActions !== void 0) {
@@ -14963,10 +16464,391 @@ async function resolveApplyTimeIdentities(blueprint, provisioned, bootstrapClien
14963
16464
  const resolved = await resolveBlueprintIdentities(blueprint, resolver);
14964
16465
  return parseBlueprint(resolved);
14965
16466
  }
14966
-
14967
- // src/loader.ts
14968
- function actionOf(created) {
14969
- return created ? "created" : "reconciled";
16467
+
16468
+ // src/scope-grammar.ts
16469
+ var FORBIDDEN_SCOPE_NAMESPACES2 = /* @__PURE__ */ new Set([
16470
+ "user",
16471
+ "record",
16472
+ "document",
16473
+ "entity",
16474
+ "self",
16475
+ "tenant",
16476
+ "context",
16477
+ "scope",
16478
+ "versions",
16479
+ "lookup"
16480
+ ]);
16481
+ var SCOPE_NAMESPACE_RE2 = /^[a-z][a-z0-9_-]{1,31}$/;
16482
+ var SCOPE_NAMESPACE_MIN_LENGTH = 2;
16483
+ var SCOPE_NAMESPACE_MAX_LENGTH = 32;
16484
+ var SCOPE_VALUE_RE2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
16485
+ var SCOPE_VALUE_MIN_LENGTH = 1;
16486
+ var SCOPE_VALUE_MAX_LENGTH = 128;
16487
+ function valueGrammarError(value) {
16488
+ if (!SCOPE_VALUE_RE2.test(value)) {
16489
+ 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.`;
16490
+ }
16491
+ return null;
16492
+ }
16493
+ function namespaceGrammarError(ns) {
16494
+ if (FORBIDDEN_SCOPE_NAMESPACES2.has(ns)) {
16495
+ return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
16496
+ }
16497
+ if (!SCOPE_NAMESPACE_RE2.test(ns)) {
16498
+ 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 '-'`;
16499
+ }
16500
+ return null;
16501
+ }
16502
+ function assertScopeEntries(entries, flag) {
16503
+ if (!entries?.length) return;
16504
+ for (const raw of entries) {
16505
+ const sep = raw.indexOf(":");
16506
+ if (sep <= 0 || sep === raw.length - 1) {
16507
+ throw new Error(`${flag} must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
16508
+ }
16509
+ const nsError = namespaceGrammarError(raw.slice(0, sep));
16510
+ if (nsError) throw new Error(`${flag} namespace ${nsError}`);
16511
+ const valError = valueGrammarError(raw.slice(sep + 1));
16512
+ if (valError) throw new Error(`${flag} value ${valError}`);
16513
+ }
16514
+ }
16515
+ function assertIdentityOverrideValues(overrides, flag) {
16516
+ if (!overrides) return;
16517
+ for (const [key, value] of Object.entries(overrides)) {
16518
+ if (!key.startsWith("scope:")) continue;
16519
+ const error = valueGrammarError(typeof value === "string" ? value : String(value));
16520
+ if (error) throw new Error(`${flag} '${key}' value ${error}`);
16521
+ }
16522
+ }
16523
+ function placeholderScopeNamespace(value) {
16524
+ const self2 = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
16525
+ if (self2) return self2[1];
16526
+ const member = MEMBER_SCOPE_PLACEHOLDER_RE2.exec(value);
16527
+ return member ? member[1] : null;
16528
+ }
16529
+ var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
16530
+ var UNDER_SELF_PLACEHOLDER_RE2 = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
16531
+ var MEMBER_SCOPE_PLACEHOLDER_RE2 = /^\$\{\{\s*member\.scope\.([a-z][a-z0-9_-]{1,31})(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
16532
+ var ROLE_ID_RE2 = /^[a-z][a-z0-9-]{2,30}$/;
16533
+ var MAX_ASSIGNABLE_ROLES2 = 20;
16534
+ function parseAssignableRoles(raw, flag = "--assignable-role") {
16535
+ if (!raw || raw.length === 0) return void 0;
16536
+ if (raw.length > MAX_ASSIGNABLE_ROLES2) {
16537
+ throw new Error(`${flag} accepts at most ${MAX_ASSIGNABLE_ROLES2} entries; got ${raw.length}.`);
16538
+ }
16539
+ const seen = /* @__PURE__ */ new Set();
16540
+ for (const roleId of raw) {
16541
+ if (!ROLE_ID_RE2.test(roleId)) {
16542
+ throw new Error(
16543
+ `${flag} '${roleId}' is not a valid roleId \u2014 3-31 characters, starting with a lowercase letter and continuing with lowercase letters, digits or '-'.`
16544
+ );
16545
+ }
16546
+ if (seen.has(roleId)) {
16547
+ throw new Error(`${flag} repeats '${roleId}' \u2014 list it once.`);
16548
+ }
16549
+ seen.add(roleId);
16550
+ }
16551
+ return [...raw];
16552
+ }
16553
+
16554
+ // src/verb-support.ts
16555
+ async function withAuth(opts, deps, body) {
16556
+ const log = deps.log ?? createLogger2();
16557
+ const resolveAuth = deps.resolveAuth ?? resolveAuthContext;
16558
+ let auth;
16559
+ try {
16560
+ auth = await resolveAuth({
16561
+ env: opts.env,
16562
+ baseUrl: opts.baseUrl,
16563
+ token: opts.token,
16564
+ context: opts.context,
16565
+ provisioning: opts.provisioning,
16566
+ tenant: opts.tenant,
16567
+ useScopedToken: opts.useScopedToken,
16568
+ log
16569
+ });
16570
+ } catch (err) {
16571
+ if (err instanceof MissingCredentialError) {
16572
+ process.stderr.write(` \u2716 ${err.message}
16573
+ `);
16574
+ return 2;
16575
+ }
16576
+ process.stderr.write(` \u2716 ${formatError(err)}
16577
+ `);
16578
+ return 1;
16579
+ }
16580
+ return body(auth, log);
16581
+ }
16582
+ function requireTenantId(auth) {
16583
+ if (!auth.tenantId) {
16584
+ throw new Error(
16585
+ "Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed."
16586
+ );
16587
+ }
16588
+ return auth.tenantId;
16589
+ }
16590
+ function normalizePrincipalUserId(principal) {
16591
+ return principal.startsWith("usr_") ? principal.slice("usr_".length) : principal;
16592
+ }
16593
+ function splitCsv(csv) {
16594
+ return csv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
16595
+ }
16596
+ function parseScopeOverrides(scopes) {
16597
+ if (!scopes || scopes.length === 0) return void 0;
16598
+ const out = {};
16599
+ for (const raw of scopes) {
16600
+ const sep = raw.indexOf(":");
16601
+ if (sep <= 0 || sep === raw.length - 1) {
16602
+ throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
16603
+ }
16604
+ const ns = raw.slice(0, sep);
16605
+ const value = raw.slice(sep + 1);
16606
+ const valueError = valueGrammarError(value);
16607
+ if (valueError) throw new Error(`--scope value ${valueError}`);
16608
+ const key = `scope:${ns}`;
16609
+ if (Object.prototype.hasOwnProperty.call(out, key)) {
16610
+ throw new Error(`--scope sets namespace '${ns}' more than once \u2014 a scope override takes at most one value per namespace.`);
16611
+ }
16612
+ out[key] = value;
16613
+ }
16614
+ if (Object.keys(out).length > 2) {
16615
+ throw new Error("--scope allows at most two distinct namespaces.");
16616
+ }
16617
+ return out;
16618
+ }
16619
+ function parseJsonObjectFlag(flag, raw) {
16620
+ let parsed;
16621
+ try {
16622
+ parsed = JSON.parse(raw);
16623
+ } catch {
16624
+ throw new Error(`${flag} is not valid JSON: ${raw}`);
16625
+ }
16626
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
16627
+ throw new Error(`${flag} must be a JSON object (e.g. '{"userId":["<id>"]}').`);
16628
+ }
16629
+ return parsed;
16630
+ }
16631
+
16632
+ // src/paginate.ts
16633
+ var PAGE_LIMIT = 100;
16634
+ async function drainPages(fetch2) {
16635
+ const out = [];
16636
+ let startFrom;
16637
+ for (; ; ) {
16638
+ const page = await fetch2(startFrom);
16639
+ if (page.data) out.push(...page.data);
16640
+ const next = page.nextCursor;
16641
+ if (!next) break;
16642
+ startFrom = next;
16643
+ }
16644
+ return out;
16645
+ }
16646
+
16647
+ // src/loader.ts
16648
+ function actionOf(created) {
16649
+ return created ? "created" : "reconciled";
16650
+ }
16651
+ function triggerPrincipalExternalId(blueprintName, triggerName) {
16652
+ return `${blueprintName}-trigger-${triggerName}`;
16653
+ }
16654
+ var SCRIPT_CONFLICT_RETRY_DELAY_MS = 750;
16655
+ function isVersionConflict(err) {
16656
+ return typeof err === "object" && err !== null && err.statusCode === 409;
16657
+ }
16658
+ async function readLatestScriptVersion(ctxClient, name) {
16659
+ const versions = await drainPages(
16660
+ (startFrom) => ctxClient.scripts.listScripts({ name, startFrom, limit: PAGE_LIMIT })
16661
+ );
16662
+ return versions.reduce((best, row) => {
16663
+ if (row.scriptVersion === void 0) return best;
16664
+ if (best === void 0 || best.scriptVersion === void 0) return row;
16665
+ return row.scriptVersion > best.scriptVersion ? row : best;
16666
+ }, void 0);
16667
+ }
16668
+ function requireScriptId(row, name) {
16669
+ if (!row.id) {
16670
+ throw new Error(`Script '${name}' listing returned a version with no id.`);
16671
+ }
16672
+ return row.id;
16673
+ }
16674
+ function scriptMatches(stored, declared) {
16675
+ return stored.source === declared.source && (stored.declaredInputContract ?? void 0) === declared.declaredInputContract;
16676
+ }
16677
+ async function ensureScripts(args) {
16678
+ const { ctxClient, scripts, log } = args;
16679
+ const actions = [];
16680
+ for (const [name, entry] of Object.entries(scripts)) {
16681
+ let latest = await readLatestScriptVersion(ctxClient, name);
16682
+ if (latest !== void 0 && scriptMatches(latest, entry)) {
16683
+ actions.push({
16684
+ name,
16685
+ action: "unchanged",
16686
+ id: requireScriptId(latest, name),
16687
+ scriptVersion: latest.scriptVersion ?? 0
16688
+ });
16689
+ log.debug(
16690
+ { component: "loader", scriptName: name, scriptVersion: latest.scriptVersion },
16691
+ "script already matches the declared source \u2014 no version pushed"
16692
+ );
16693
+ continue;
16694
+ }
16695
+ const body = { name, source: entry.source, declaredInputContract: entry.declaredInputContract };
16696
+ let pushed;
16697
+ try {
16698
+ pushed = await ctxClient.scripts.createScript(body);
16699
+ } catch (err) {
16700
+ if (!isVersionConflict(err)) throw err;
16701
+ log.debug(
16702
+ { component: "loader", scriptName: name },
16703
+ "concurrent push detected (409) \u2014 pausing, then re-reading the current version"
16704
+ );
16705
+ await new Promise((resolve) => setTimeout(resolve, SCRIPT_CONFLICT_RETRY_DELAY_MS));
16706
+ try {
16707
+ latest = await readLatestScriptVersion(ctxClient, name);
16708
+ } catch (readErr) {
16709
+ throw new Error(
16710
+ `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.`
16711
+ );
16712
+ }
16713
+ if (latest !== void 0 && scriptMatches(latest, entry)) {
16714
+ actions.push({
16715
+ name,
16716
+ action: "unchanged",
16717
+ id: requireScriptId(latest, name),
16718
+ scriptVersion: latest.scriptVersion ?? 0
16719
+ });
16720
+ log.debug(
16721
+ { component: "loader", scriptName: name, scriptVersion: latest.scriptVersion },
16722
+ "the concurrent push stored the same source \u2014 converged, nothing to do"
16723
+ );
16724
+ continue;
16725
+ }
16726
+ try {
16727
+ pushed = await ctxClient.scripts.createScript(body);
16728
+ } catch (retryErr) {
16729
+ if (!isVersionConflict(retryErr)) throw retryErr;
16730
+ throw new Error(
16731
+ `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.`
16732
+ );
16733
+ }
16734
+ }
16735
+ if (!pushed.id) {
16736
+ throw new Error(`Script '${name}' push returned no id.`);
16737
+ }
16738
+ const firstVersion = latest === void 0;
16739
+ actions.push({
16740
+ name,
16741
+ action: firstVersion ? "created" : "superseded",
16742
+ id: pushed.id,
16743
+ scriptVersion: pushed.scriptVersion ?? 0
16744
+ });
16745
+ log.debug(
16746
+ { component: "loader", scriptName: name, scriptVersion: pushed.scriptVersion },
16747
+ firstVersion ? "script pushed \u2014 first version for this name in this context" : "script source changed \u2014 new version pushed over an existing name"
16748
+ );
16749
+ }
16750
+ return actions;
16751
+ }
16752
+ async function pruneUndeclaredTriggers(args) {
16753
+ const { ctxClient, blueprintName, contextId, declaredNames, log } = args;
16754
+ const pruned = [];
16755
+ const skipped = [];
16756
+ let live;
16757
+ try {
16758
+ live = await drainPages(
16759
+ (startFrom) => ctxClient.triggers.listTriggers({ startFrom, limit: PAGE_LIMIT })
16760
+ );
16761
+ } catch (err) {
16762
+ log.warn(
16763
+ { component: "loader", err: extractServerError(err) },
16764
+ "could not list trigger rules \u2014 skipping the trigger reconcile entirely; a trigger removed from this blueprint may still be live"
16765
+ );
16766
+ return { pruned, skipped: [{ reason: "the context's trigger rules could not be listed" }] };
16767
+ }
16768
+ for (const row of live) {
16769
+ if (!row.name || !row.id || !row.principalId) {
16770
+ if (!declaredNames.has(row.name ?? "")) {
16771
+ skipped.push({
16772
+ name: row.name,
16773
+ reason: "the trigger rule carries no id/name/principal, so it cannot be safely de-provisioned"
16774
+ });
16775
+ }
16776
+ continue;
16777
+ }
16778
+ if (declaredNames.has(row.name)) continue;
16779
+ if (!row.provisionedBy) {
16780
+ skipped.push({
16781
+ name: row.name,
16782
+ reason: "it carries no blueprint provenance marker, so it cannot be attributed to this blueprint"
16783
+ });
16784
+ continue;
16785
+ }
16786
+ if (row.provisionedBy !== blueprintName) {
16787
+ log.debug(
16788
+ { component: "loader", triggerName: row.name, provisionedBy: row.provisionedBy },
16789
+ "undeclared trigger left untouched \u2014 provisioned by a different blueprint"
16790
+ );
16791
+ continue;
16792
+ }
16793
+ let principalExternalId;
16794
+ try {
16795
+ principalExternalId = (await ctxClient.identity.getUser({ id: normalizePrincipalUserId(row.principalId) })).externalId;
16796
+ } catch (err) {
16797
+ log.warn(
16798
+ { component: "loader", triggerName: row.name, principalId: row.principalId, err: extractServerError(err) },
16799
+ "could not resolve a trigger's principal \u2014 leaving the trigger in place"
16800
+ );
16801
+ skipped.push({
16802
+ name: row.name,
16803
+ reason: "its service principal could not be read, so ownership could not be fully established"
16804
+ });
16805
+ continue;
16806
+ }
16807
+ if (principalExternalId !== triggerPrincipalExternalId(blueprintName, row.name)) {
16808
+ log.warn(
16809
+ { component: "loader", triggerName: row.name, principalId: row.principalId },
16810
+ "undeclared trigger left untouched \u2014 it carries this blueprint's provenance marker but names a principal this blueprint did not mint for it"
16811
+ );
16812
+ skipped.push({
16813
+ name: row.name,
16814
+ reason: "it names a service principal this blueprint did not mint for a trigger of that name, so its provenance is inconsistent"
16815
+ });
16816
+ continue;
16817
+ }
16818
+ let profileDeleted = true;
16819
+ try {
16820
+ await ctxClient.auth.deleteAccessProfile({ contextId, principalId: row.principalId });
16821
+ } catch (err) {
16822
+ if (!isNotFound(err)) {
16823
+ log.warn(
16824
+ { component: "loader", triggerName: row.name, principalId: row.principalId, err: extractServerError(err) },
16825
+ "could not delete a removed trigger's access profile \u2014 leaving the trigger rule in place so the next apply retries"
16826
+ );
16827
+ skipped.push({ name: row.name, reason: "its access profile could not be deleted, so its grant may still be live" });
16828
+ continue;
16829
+ }
16830
+ profileDeleted = false;
16831
+ }
16832
+ try {
16833
+ await ctxClient.triggers.deleteTrigger({ id: row.id });
16834
+ } catch (err) {
16835
+ log.warn(
16836
+ { component: "loader", triggerName: row.name, err: extractServerError(err) },
16837
+ "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"
16838
+ );
16839
+ skipped.push({ name: row.name, reason: "its grant was removed but the rule row itself could not be deleted" });
16840
+ continue;
16841
+ }
16842
+ pruned.push({ name: row.name, principalId: row.principalId, profileDeleted });
16843
+ log.debug(
16844
+ { component: "loader", triggerName: row.name, principalId: row.principalId, profileDeleted },
16845
+ "undeclared trigger de-provisioned (no longer declared by this blueprint)"
16846
+ );
16847
+ }
16848
+ return { pruned, skipped };
16849
+ }
16850
+ function isNotFound(err) {
16851
+ return typeof err === "object" && err !== null && err.statusCode === 404;
14970
16852
  }
14971
16853
  async function registerNamespaceIdempotently(client, blueprintContextId, decl) {
14972
16854
  const contextIdForRequest = decl.tenantWide ? void 0 : blueprintContextId;
@@ -15070,6 +16952,10 @@ function toSdkFields(blueprint, schemaIndex) {
15070
16952
  required: f.required,
15071
16953
  searchable: f.searchable,
15072
16954
  filterable: f.filterable,
16955
+ // Forwarded explicitly, like every sibling here: this mapping is a named-property list, not a
16956
+ // spread, so an unforwarded field is dropped SILENTLY rather than failing — which is what
16957
+ // happened to `validation` before it was added below.
16958
+ inline: f.inline,
15073
16959
  description: f.description,
15074
16960
  enumValues: mapEnumValues(f.enumValues),
15075
16961
  // Format passthrough — the loader previously DROPPED this (the
@@ -15125,13 +17011,16 @@ function toCapabilities(capabilities) {
15125
17011
  if (!capabilities) return void 0;
15126
17012
  const out = {};
15127
17013
  if (capabilities.auditHistory !== void 0) out.auditHistory = capabilities.auditHistory;
17014
+ if (capabilities.triggersEnabled !== void 0)
17015
+ out.triggersEnabled = capabilities.triggersEnabled;
15128
17016
  return Object.keys(out).length > 0 ? out : void 0;
15129
17017
  }
15130
17018
  function toRoleScopeClauses(clauses) {
15131
17019
  return clauses.map((c) => ({
15132
17020
  allowed_actions: c.allowedActions,
15133
17021
  data_scope: c.dataScope,
15134
- granted_capabilities: c.capabilities
17022
+ granted_capabilities: c.capabilities,
17023
+ assignable_roles: c.assignableRoles
15135
17024
  }));
15136
17025
  }
15137
17026
  var UnresolvedIdentitiesError = class extends Error {
@@ -15168,7 +17057,13 @@ async function provisionContext(client, input) {
15168
17057
  const { blueprint, allowTenantWideNamespaces = false, readExistingKey, confirmExistingPrincipal, log } = input;
15169
17058
  assertBlueprintGates(blueprint);
15170
17059
  assertTenantWideNamespacesAllowed(blueprint, allowTenantWideNamespaces);
15171
- const ctxResp = await client.auth.createAppContext({ body: { contextId: blueprint.contextId, name: contextNameOf(blueprint) } });
17060
+ const ctxResp = await client.auth.createAppContext({
17061
+ body: {
17062
+ contextId: blueprint.contextId,
17063
+ name: contextNameOf(blueprint),
17064
+ ...blueprint.identityProjectionClaims === void 0 ? {} : { identityProjectionClaims: blueprint.identityProjectionClaims }
17065
+ }
17066
+ });
15172
17067
  const contextAction = actionOf(ctxResp.created);
15173
17068
  log.debug({ component: "loader", contextId: blueprint.contextId, contextAction }, "app-context ensured");
15174
17069
  const user = await client.identity.createUser({
@@ -15255,7 +17150,8 @@ async function runLoader(client, input) {
15255
17150
  subClaim: issuer.subClaim,
15256
17151
  emailClaim: issuer.emailClaim,
15257
17152
  userinfoUri: issuer.userinfoUri,
15258
- selfSignupPolicies
17153
+ selfSignupPolicies,
17154
+ capturedClaims: issuer.capturedClaims
15259
17155
  });
15260
17156
  const action = actionOf(issuerResp.created);
15261
17157
  issuerActions.push({ issuerId: issuer.issuerId, action });
@@ -15271,7 +17167,8 @@ async function runLoader(client, input) {
15271
17167
  subClaim: issuer.subClaim,
15272
17168
  emailClaim: issuer.emailClaim,
15273
17169
  userinfoUri: issuer.userinfoUri,
15274
- selfSignupPolicies
17170
+ selfSignupPolicies,
17171
+ capturedClaims: issuer.capturedClaims
15275
17172
  });
15276
17173
  log.debug({ component: "loader", issuerId: issuer.issuerId }, "issuer already registered \u2014 reconciled safe fields to the declared values");
15277
17174
  } else {
@@ -15352,7 +17249,10 @@ async function runLoader(client, input) {
15352
17249
  data_scope: blueprint.accessProfile.dataScope,
15353
17250
  // Already gate-validated (assertBlueprintGates, above this function's
15354
17251
  // call site) before this body is ever built.
15355
- granted_capabilities: blueprint.accessProfile.capabilities
17252
+ granted_capabilities: blueprint.accessProfile.capabilities,
17253
+ // Absent means "no roleId restriction" server-side; an empty list is refused. Undeclared
17254
+ // must therefore stay `undefined` rather than becoming `[]`.
17255
+ assignable_roles: blueprint.accessProfile.assignableRoles
15356
17256
  }
15357
17257
  ],
15358
17258
  // Optional identity overrides — scope values the service key STAMPS onto
@@ -15375,6 +17275,109 @@ async function runLoader(client, input) {
15375
17275
  body: profileBody
15376
17276
  });
15377
17277
  log.debug({ component: "loader", principalId: `usr_${userId}`, accessProfileId: profile.id, profileAction }, "access profile ensured + reconciled to declared scopes");
17278
+ const scriptActions = blueprint.scripts ? await ensureScripts({ ctxClient, scripts: blueprint.scripts, log }) : [];
17279
+ const triggerActions = [];
17280
+ for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
17281
+ const schemaId = schemaIds[entry.firingSource.schemaName];
17282
+ if (!schemaId) {
17283
+ throw new Error(
17284
+ `Trigger '${triggerName}' names schema '${entry.firingSource.schemaName}', which has no resolved schemaId \u2014 the schema failed to provision.`
17285
+ );
17286
+ }
17287
+ const triggerUser = await client.identity.createUser({
17288
+ body: {
17289
+ externalId: triggerPrincipalExternalId(blueprint.name, triggerName),
17290
+ type: "SERVICE",
17291
+ payload: { displayName: `Trigger: ${triggerName}` }
17292
+ }
17293
+ });
17294
+ const triggerUserId = triggerUser.id;
17295
+ if (!triggerUserId) {
17296
+ throw new Error(`Trigger '${triggerName}' service-principal create returned no user id.`);
17297
+ }
17298
+ const triggerPlaceholderProfileBody = {
17299
+ principalId: `usr_${triggerUserId}`,
17300
+ scopes: [{ allowed_actions: ["triggers:r"] }]
17301
+ };
17302
+ const triggerProfile = await ctxClient.auth.createAccessProfile({
17303
+ contextId: blueprint.contextId,
17304
+ body: triggerPlaceholderProfileBody
17305
+ });
17306
+ log.debug(
17307
+ { component: "loader", triggerName, principalId: `usr_${triggerUserId}`, accessProfileId: triggerProfile.id },
17308
+ "trigger principal + placeholder access profile ensured (no declared grant attached yet)"
17309
+ );
17310
+ const triggerResp = await ctxClient.triggers.createTrigger({
17311
+ upsert: true,
17312
+ body: {
17313
+ name: triggerName,
17314
+ firingSource: { schemaId, event: entry.firingSource.event },
17315
+ scriptRef: entry.scriptRef,
17316
+ roleIds: entry.roleIds,
17317
+ scopes: entry.roleIds ? void 0 : [
17318
+ {
17319
+ allowed_actions: entry.allowedActions,
17320
+ data_scope: entry.dataScope,
17321
+ granted_capabilities: entry.capabilities
17322
+ }
17323
+ ],
17324
+ manifest: entry.manifest,
17325
+ // REQUIRED on create. `[]` is a real declaration ("project nothing beyond input.recordId"),
17326
+ // not an omission, so it is forwarded as-is rather than collapsed to undefined the way an
17327
+ // absent optional would be — the blueprint schema makes it required precisely so an author
17328
+ // STATES it.
17329
+ fields: entry.fields,
17330
+ principalId: `usr_${triggerUserId}`,
17331
+ // The PROVENANCE MARKER the reconcile-by-absence pass gates its deletes on. Stamped on every
17332
+ // apply (not only the first) so a trigger provisioned before this field existed acquires one
17333
+ // the next time its blueprint is applied — the platform accepts a set-when-absent and refuses
17334
+ // a CHANGE, so re-stating it is idempotent and can never re-point someone else's trigger.
17335
+ provisionedBy: blueprint.name
17336
+ }
17337
+ });
17338
+ triggerActions.push({ name: triggerName, action: actionOf(triggerResp.created) });
17339
+ log.debug({ component: "loader", triggerName, action: actionOf(triggerResp.created) }, "trigger rule ensured (upsert)");
17340
+ const triggerProfileBody = entry.roleIds ? { principalId: `usr_${triggerUserId}`, roleIds: entry.roleIds } : {
17341
+ principalId: `usr_${triggerUserId}`,
17342
+ scopes: [
17343
+ {
17344
+ // Non-null: the `else` branch of the roleIds XOR above — BlueprintTriggerEntrySchema
17345
+ // guarantees allowedActions is set whenever roleIds is not.
17346
+ allowed_actions: entry.allowedActions,
17347
+ data_scope: entry.dataScope,
17348
+ granted_capabilities: entry.capabilities
17349
+ }
17350
+ ]
17351
+ };
17352
+ await ctxClient.auth.updateAccessProfile({
17353
+ contextId: blueprint.contextId,
17354
+ principalId: `usr_${triggerUserId}`,
17355
+ body: triggerProfileBody
17356
+ });
17357
+ log.debug(
17358
+ { component: "loader", triggerName, principalId: `usr_${triggerUserId}` },
17359
+ "trigger access profile widened to its declared grant"
17360
+ );
17361
+ }
17362
+ let triggerPrunes = [];
17363
+ let triggerPruneSkipped = [];
17364
+ if (input.pruneTriggers === false) {
17365
+ log.debug({ component: "loader" }, "trigger reconcile-by-absence skipped \u2014 disabled by the caller");
17366
+ } else if (!input.remintForContext) {
17367
+ log.warn(
17368
+ { component: "loader", contextId: blueprint.contextId },
17369
+ "trigger reconcile-by-absence skipped \u2014 no per-context client is wired, so a removed trigger may still be live"
17370
+ );
17371
+ triggerPruneSkipped = [{ reason: "no per-context client was wired, so triggers could not be reconciled in the blueprint's own context" }];
17372
+ } else {
17373
+ ({ pruned: triggerPrunes, skipped: triggerPruneSkipped } = await pruneUndeclaredTriggers({
17374
+ ctxClient,
17375
+ blueprintName: blueprint.name,
17376
+ contextId: blueprint.contextId,
17377
+ declaredNames: new Set(Object.keys(blueprint.triggers ?? {})),
17378
+ log
17379
+ }));
17380
+ }
15378
17381
  const keyName = keyNameFor(blueprint.name, machineId);
15379
17382
  let keyAction = "created";
15380
17383
  let revokedKeyId;
@@ -15496,6 +17499,10 @@ async function runLoader(client, input) {
15496
17499
  profileAction,
15497
17500
  schemaActions,
15498
17501
  roleActions,
17502
+ scriptActions,
17503
+ triggerActions,
17504
+ triggerPrunes,
17505
+ triggerPruneSkipped,
15499
17506
  issuerActions,
15500
17507
  namespaceActions,
15501
17508
  rawKey,
@@ -15516,7 +17523,11 @@ function renderPlanDataScope(ds) {
15516
17523
  }
15517
17524
  function buildPlan(blueprint, opts = {}) {
15518
17525
  const steps = [];
15519
- steps.push({ kind: "context", summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)})` });
17526
+ const projection = blueprint.identityProjectionClaims?.length ? `, identity projection: [${blueprint.identityProjectionClaims.join(", ")}]` : blueprint.identityProjectionClaims ? ", identity projection: disabled (empty list)" : "";
17527
+ steps.push({
17528
+ kind: "context",
17529
+ summary: `context '${blueprint.contextId}' (${contextNameOf(blueprint)}${projection})`
17530
+ });
15520
17531
  steps.push({
15521
17532
  kind: "principal",
15522
17533
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
@@ -15532,9 +17543,10 @@ function buildPlan(blueprint, opts = {}) {
15532
17543
  }
15533
17544
  for (const iss of blueprint.issuers ?? []) {
15534
17545
  const selfSignup = iss.selfSignupPolicies?.length ? `, self-signup: ${iss.selfSignupPolicies.length} polic${iss.selfSignupPolicies.length === 1 ? "y" : "ies"}` : "";
17546
+ const captured = iss.capturedClaims?.length ? `, captured claims: [${iss.capturedClaims.join(", ")}]` : "";
15535
17547
  steps.push({
15536
17548
  kind: "issuer",
15537
- summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
17549
+ summary: `issuer '${iss.issuerId}' (${iss.issuer} \u2192 context '${iss.contextId}'${selfSignup}${captured}) [${BLUEPRINT_FIELD_PHASES.issuers}-token phase]`
15538
17550
  });
15539
17551
  }
15540
17552
  for (const s of blueprint.schemas) {
@@ -15556,7 +17568,8 @@ function buildPlan(blueprint, opts = {}) {
15556
17568
  const roleCapabilityNote = capSet.length ? `, capabilities [${capSet.join(", ")}]` : "";
15557
17569
  const clauseLines = clauses.map((c, i) => {
15558
17570
  const capNote = c.capabilities?.length ? `, capabilities [${c.capabilities.join(", ")}]` : "";
15559
- return ` clause ${i + 1}: actions [${c.allowedActions.join(", ")}], dataScope: ${renderPlanDataScope(c.dataScope)}${capNote}`;
17571
+ const assignableNote = c.assignableRoles?.length ? `, assignableRoles [${c.assignableRoles.join(", ")}]` : "";
17572
+ return ` clause ${i + 1}: actions [${c.allowedActions.join(", ")}], dataScope: ${renderPlanDataScope(c.dataScope)}${capNote}${assignableNote}`;
15560
17573
  });
15561
17574
  const assumable = blueprint.roleAssumable?.[roleId];
15562
17575
  const assumableLine = assumable ? `
@@ -15586,6 +17599,43 @@ function buildPlan(blueprint, opts = {}) {
15586
17599
  summary: `profile scopes [${actions.join(", ")}]${planeNote}${scopeNote}${capabilityNote}`
15587
17600
  });
15588
17601
  }
17602
+ for (const [scriptName, entry] of Object.entries(blueprint.scripts ?? {})) {
17603
+ const bytes = new TextEncoder().encode(entry.source).length;
17604
+ const contractNote = entry.declaredInputContract ? " + input contract" : "";
17605
+ steps.push({
17606
+ kind: "script",
17607
+ summary: `script '${scriptName}' (ensure latest matches ${bytes} bytes of source${contractNote}) [${BLUEPRINT_FIELD_PHASES.scripts}-token phase]`
17608
+ });
17609
+ }
17610
+ for (const [triggerName, entry] of Object.entries(blueprint.triggers ?? {})) {
17611
+ steps.push({
17612
+ kind: "principal",
17613
+ summary: `trigger '${triggerName}' principal '${blueprint.name}-trigger-${triggerName}' (SERVICE)`
17614
+ });
17615
+ steps.push({
17616
+ kind: "profile",
17617
+ summary: `trigger '${triggerName}' profile: minimal placeholder grant (minted before the trigger row exists)`
17618
+ });
17619
+ const manifestNote = entry.manifest?.length ? ` manifest: [${entry.manifest.join(", ")}]` : " manifest: (none \u2014 full grant applies)";
17620
+ const fieldsNote = entry.fields?.length ? ` projects: [${entry.fields.join(", ")}]` : " projects: (nothing beyond the record id)";
17621
+ steps.push({
17622
+ kind: "trigger",
17623
+ summary: `trigger '${triggerName}': ${entry.firingSource.schemaName}.${entry.firingSource.event} \u2192 ${entry.scriptRef.name}@${entry.scriptRef.version}${fieldsNote}${manifestNote} [${BLUEPRINT_FIELD_PHASES.triggers}-token phase]`
17624
+ });
17625
+ if (entry.roleIds) {
17626
+ steps.push({
17627
+ kind: "profile",
17628
+ summary: `trigger '${triggerName}' profile widened via role(s) [${entry.roleIds.join(", ")}] (composed)`
17629
+ });
17630
+ } else {
17631
+ const scopeNote = entry.dataScope && Object.keys(entry.dataScope).length ? ` dataScope: ${Object.keys(entry.dataScope).join(", ")}` : "";
17632
+ const capabilityNote = entry.capabilities?.length ? ` capabilities: [${entry.capabilities.join(", ")}]` : "";
17633
+ steps.push({
17634
+ kind: "profile",
17635
+ summary: `trigger '${triggerName}' profile widened to declared scopes [${(entry.allowedActions ?? []).join(", ")}]${scopeNote}${capabilityNote}`
17636
+ });
17637
+ }
17638
+ }
15589
17639
  steps.push({ kind: "key", summary: `key ${blueprint.name}-<machine> (mint 1 scoped ssk_*)` });
15590
17640
  const seeds = blueprint.seed ?? [];
15591
17641
  if (seeds.length > 0 && !opts.noSeed) {
@@ -15601,9 +17651,27 @@ function buildPlan(blueprint, opts = {}) {
15601
17651
  function renderPlan(blueprint, steps) {
15602
17652
  const header = ` Plan for blueprint '${blueprint.name}' (v${blueprint.version}) \u2192 context '${blueprint.contextId}':`;
15603
17653
  const legendLines = [
15604
- ` Apply is a non-destructive reconcile \u2014 each item is created if absent, else`,
15605
- ` updated in place to match. Nothing is dropped or recreated.`
17654
+ ` Apply is a reconcile \u2014 each item is created if absent, else updated in place to`,
17655
+ ` match. Nothing is recreated, and nothing is dropped except as noted below.`
15606
17656
  ];
17657
+ legendLines.push(
17658
+ ` Exception: triggers are also reconciled by ABSENCE \u2014 a trigger this blueprint`,
17659
+ ` previously provisioned and no longer declares is DELETED (the rule and its access`,
17660
+ ` profile; its service principal is kept). This applies even when this plan shows no`,
17661
+ ` trigger steps: removing the last one is exactly what leaves none to show. Triggers`,
17662
+ ` created outside this blueprint are never touched. Each deletion is named in the`,
17663
+ ` apply output.`
17664
+ );
17665
+ legendLines.push(
17666
+ ` Counter-exception: a script is never updated in place \u2014 a changed source ADDS a new`,
17667
+ ` immutable version and every earlier one persists, which is also how a 'latest' rule`,
17668
+ ` picks the change up. And scripts are NOT reconciled by absence: a script this blueprint`,
17669
+ ` previously shipped and no longer declares is LEFT IN PLACE, including every stored`,
17670
+ ` version. A script row carries no provisioning marker, so an apply cannot tell one it`,
17671
+ ` created from one you pushed by hand \u2014 and a wrong delete is unrecoverable and would`,
17672
+ ` break any trigger still referencing it. Remove one deliberately with`,
17673
+ ` 'vectros scripts delete', after 'vectros scripts list' shows you what is there.`
17674
+ );
15607
17675
  if (steps.some((s) => s.kind === "issuer")) {
15608
17676
  legendLines.push(
15609
17677
  ` Exception: an issuer's trust-anchor fields (issuer, jwksUri, audience,`,
@@ -17855,6 +19923,19 @@ function namespaceTally(items) {
17855
19923
  if (matched) seg.push(`${matched} already registered \u2014 confirmed matching`);
17856
19924
  return `${items.length} namespace${items.length === 1 ? "" : "s"} (${seg.join(", ")})`;
17857
19925
  }
19926
+ function scriptTally(items) {
19927
+ if (items.length === 0) return null;
19928
+ const created = items.filter((i) => i.action === "created");
19929
+ const superseded = items.filter((i) => i.action === "superseded");
19930
+ const unchanged = items.length - created.length - superseded.length;
19931
+ const seg = [];
19932
+ if (created.length) seg.push(`${created.length} first version${created.length === 1 ? "" : "s"} pushed`);
19933
+ if (superseded.length) {
19934
+ seg.push(`${superseded.map((i) => `'${i.name}'`).join(", ")} superseded (a new version now current)`);
19935
+ }
19936
+ if (unchanged) seg.push(`${unchanged} already current \u2014 nothing written`);
19937
+ return `${items.length} script${items.length === 1 ? "" : "s"} (${seg.join("; ")})`;
19938
+ }
17858
19939
  function applyOutcomeSummary(result) {
17859
19940
  const parts = [
17860
19941
  reconcileTally(result.schemaActions, "schema"),
@@ -17863,13 +19944,34 @@ function applyOutcomeSummary(result) {
17863
19944
  // 'reconciled' mean the same "updated to match" as every other kind here.
17864
19945
  reconcileTally(result.issuerActions, "issuer"),
17865
19946
  namespaceTally(result.namespaceActions),
19947
+ // Scripts — reported BEFORE triggers, mirroring the order they are provisioned in, and with
19948
+ // their own wording because "reconciled" would be a lie: a script version is immutable.
19949
+ scriptTally(result.scriptActions),
19950
+ // Trigger rules — 'reconciled' means the same "upsert applied the declared shape" as
19951
+ // schemas/roles (createTrigger's own upsert:true reconcile), so the shared reconcileTally
19952
+ // wording applies unchanged; buildPlan's dry-run preview already discloses each trigger's own
19953
+ // steps individually, this is the apply-time tally counterpart.
19954
+ reconcileTally(result.triggerActions, "trigger"),
17866
19955
  `profile ${result.profileAction}`
17867
19956
  ].filter((p) => p !== null);
17868
19957
  const detail = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
17869
19958
  const principalNote = result.userAction === "reconciled" ? ` \u26A0 service principal already existed in this tenant before this run (not newly created)
17870
19959
  ` : "";
19960
+ const pruneNotes = result.triggerPrunes.map(
19961
+ (p) => ` \u26A0 trigger '${p.name}' removed from the blueprint \u2014 de-provisioned (rule + access profile deleted)
19962
+ ` + // Not a warning: 404 on the profile means it was already gone, so the end state is exactly
19963
+ // what a clean prune produces. Said out loud only so the absent second delete isn't read as
19964
+ // a silent skip. Every OTHER profile-delete failure lands in triggerPruneSkipped instead.
19965
+ (p.profileDeleted ? "" : ` (its access profile was already absent \u2014 nothing left to remove)
19966
+ `)
19967
+ ).join("");
19968
+ const pruneSkippedNotes = result.triggerPruneSkipped.map(
19969
+ (s) => s.name ? ` \u26A0 trigger '${s.name}' is no longer declared but could NOT be removed \u2014 ${s.reason}. It is still live.
19970
+ ` : ` \u26A0 triggers could not be reconciled this run \u2014 ${s.reason}. A trigger removed from this blueprint may still be live.
19971
+ `
19972
+ ).join("");
17871
19973
  return ` \u2714 context '${result.contextId}' ${result.contextAction}${detail}
17872
- ${principalNote}`;
19974
+ ${principalNote}${pruneNotes}${pruneSkippedNotes}`;
17873
19975
  }
17874
19976
  var BOOTSTRAP_HELP = `vectros bootstrap \u2014 provision a least-privilege MCP credential.
17875
19977
 
@@ -18068,7 +20170,7 @@ ${BOOTSTRAP_HELP}
18068
20170
  process.stderr.write(`
18069
20171
  \u2716 Scope gate refused blueprint '${blueprint.name}':
18070
20172
  `);
18071
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20173
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
18072
20174
  `);
18073
20175
  process.stderr.write(" Minting nothing.\n");
18074
20176
  return 2;
@@ -18077,7 +20179,7 @@ ${BOOTSTRAP_HELP}
18077
20179
  process.stderr.write(`
18078
20180
  \u2716 Capability gate refused blueprint '${blueprint.name}':
18079
20181
  `);
18080
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20182
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
18081
20183
  `);
18082
20184
  process.stderr.write(" Minting nothing.\n");
18083
20185
  return 2;
@@ -18391,71 +20493,6 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
18391
20493
  // src/cli-blueprint.ts
18392
20494
  var import_promises3 = require("fs/promises");
18393
20495
 
18394
- // src/scope-grammar.ts
18395
- var FORBIDDEN_SCOPE_NAMESPACES2 = /* @__PURE__ */ new Set([
18396
- "user",
18397
- "record",
18398
- "document",
18399
- "entity",
18400
- "self",
18401
- "tenant",
18402
- "context",
18403
- "scope",
18404
- "versions",
18405
- "lookup"
18406
- ]);
18407
- var SCOPE_NAMESPACE_RE2 = /^[a-z][a-z0-9_-]{1,31}$/;
18408
- var SCOPE_NAMESPACE_MIN_LENGTH = 2;
18409
- var SCOPE_NAMESPACE_MAX_LENGTH = 32;
18410
- var SCOPE_VALUE_RE2 = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
18411
- var SCOPE_VALUE_MIN_LENGTH = 1;
18412
- var SCOPE_VALUE_MAX_LENGTH = 128;
18413
- function valueGrammarError(value) {
18414
- if (!SCOPE_VALUE_RE2.test(value)) {
18415
- 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.`;
18416
- }
18417
- return null;
18418
- }
18419
- function namespaceGrammarError(ns) {
18420
- if (FORBIDDEN_SCOPE_NAMESPACES2.has(ns)) {
18421
- return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
18422
- }
18423
- if (!SCOPE_NAMESPACE_RE2.test(ns)) {
18424
- 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 '-'`;
18425
- }
18426
- return null;
18427
- }
18428
- function assertScopeEntries(entries, flag) {
18429
- if (!entries?.length) return;
18430
- for (const raw of entries) {
18431
- const sep = raw.indexOf(":");
18432
- if (sep <= 0 || sep === raw.length - 1) {
18433
- throw new Error(`${flag} must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
18434
- }
18435
- const nsError = namespaceGrammarError(raw.slice(0, sep));
18436
- if (nsError) throw new Error(`${flag} namespace ${nsError}`);
18437
- const valError = valueGrammarError(raw.slice(sep + 1));
18438
- if (valError) throw new Error(`${flag} value ${valError}`);
18439
- }
18440
- }
18441
- function assertIdentityOverrideValues(overrides, flag) {
18442
- if (!overrides) return;
18443
- for (const [key, value] of Object.entries(overrides)) {
18444
- if (!key.startsWith("scope:")) continue;
18445
- const error = valueGrammarError(typeof value === "string" ? value : String(value));
18446
- if (error) throw new Error(`${flag} '${key}' value ${error}`);
18447
- }
18448
- }
18449
- function placeholderScopeNamespace(value) {
18450
- const self2 = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
18451
- if (self2) return self2[1];
18452
- const member = MEMBER_SCOPE_PLACEHOLDER_RE2.exec(value);
18453
- return member ? member[1] : null;
18454
- }
18455
- var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
18456
- var UNDER_SELF_PLACEHOLDER_RE2 = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
18457
- var MEMBER_SCOPE_PLACEHOLDER_RE2 = /^\$\{\{\s*member\.scope\.([a-z][a-z0-9_-]{1,31})(?::[a-z][a-z0-9_-]{1,31})?\s*\}\}$/;
18458
-
18459
20496
  // src/lint.ts
18460
20497
  var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
18461
20498
  var BUILT_IN_SORT_BY = /* @__PURE__ */ new Set(["createdAt", "lastUpdated"]);
@@ -18604,7 +20641,7 @@ function lintBlueprint(bp) {
18604
20641
  );
18605
20642
  }
18606
20643
  for (const roleId of Object.keys(bp.roles ?? {})) {
18607
- if (!ROLE_ID_RE.test(roleId)) {
20644
+ if (!ROLE_ID_RE3.test(roleId)) {
18608
20645
  findings.push({
18609
20646
  severity: "error",
18610
20647
  rule: "role-id-format",
@@ -18651,7 +20688,7 @@ function lintBlueprint(bp) {
18651
20688
  }
18652
20689
  return findings;
18653
20690
  }
18654
- var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
20691
+ var ROLE_ID_RE3 = /^[a-z][a-z0-9-]{2,30}$/;
18655
20692
  var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
18656
20693
  var RECOGNIZED_PLACEHOLDER_RES = [
18657
20694
  /^\$\{\{\s*any\s*\}\}$/,
@@ -18917,14 +20954,14 @@ async function resolveAndValidate(arg, verb, supplied) {
18917
20954
  if (err instanceof ScopeGateError) {
18918
20955
  process.stderr.write(` \u2716 Scope gate rejected '${blueprint.name}':
18919
20956
  `);
18920
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20957
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
18921
20958
  `);
18922
20959
  return null;
18923
20960
  }
18924
20961
  if (err instanceof CapabilityGateError) {
18925
20962
  process.stderr.write(` \u2716 Capability gate rejected '${blueprint.name}':
18926
20963
  `);
18927
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
20964
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
18928
20965
  `);
18929
20966
  return null;
18930
20967
  }
@@ -19068,137 +21105,44 @@ async function runBlueprintCli(argv, deps = {}) {
19068
21105
  args = parseBlueprintArgs(argv);
19069
21106
  } catch (err) {
19070
21107
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}
19071
-
19072
- ${BLUEPRINT_HELP}
19073
- `);
19074
- return 1;
19075
- }
19076
- if (args.help) {
19077
- process.stdout.write(`${BLUEPRINT_HELP}
19078
- `);
19079
- return 0;
19080
- }
19081
- if (!args.sub) {
19082
- process.stdout.write(`${BLUEPRINT_HELP}
19083
- `);
19084
- return 1;
19085
- }
19086
- switch (args.sub) {
19087
- case "init":
19088
- return runInit(args);
19089
- case "validate":
19090
- return runValidate(args);
19091
- case "plan":
19092
- return runPlan(args);
19093
- case "list":
19094
- return runList();
19095
- default:
19096
- process.stderr.write(
19097
- `Unknown blueprint subcommand: ${args.sub}. Bundled: ${BLUEPRINT_NAMES.join(", ")}
19098
-
19099
- ${BLUEPRINT_HELP}
19100
- `
19101
- );
19102
- return 1;
19103
- }
19104
- }
19105
-
19106
- // src/program.ts
19107
- var import_commander = require("commander");
19108
-
19109
- // src/verb-support.ts
19110
- async function withAuth(opts, deps, body) {
19111
- const log = deps.log ?? createLogger2();
19112
- const resolveAuth = deps.resolveAuth ?? resolveAuthContext;
19113
- let auth;
19114
- try {
19115
- auth = await resolveAuth({
19116
- env: opts.env,
19117
- baseUrl: opts.baseUrl,
19118
- token: opts.token,
19119
- context: opts.context,
19120
- provisioning: opts.provisioning,
19121
- tenant: opts.tenant,
19122
- useScopedToken: opts.useScopedToken,
19123
- log
19124
- });
19125
- } catch (err) {
19126
- if (err instanceof MissingCredentialError) {
19127
- process.stderr.write(` \u2716 ${err.message}
19128
- `);
19129
- return 2;
19130
- }
19131
- process.stderr.write(` \u2716 ${formatError(err)}
19132
- `);
19133
- return 1;
19134
- }
19135
- return body(auth, log);
19136
- }
19137
- function requireTenantId(auth) {
19138
- if (!auth.tenantId) {
19139
- throw new Error(
19140
- "Could not resolve tenantId from /v1/ping \u2014 the extended ping (tenant_id) must be deployed."
19141
- );
19142
- }
19143
- return auth.tenantId;
19144
- }
19145
- function normalizePrincipalUserId(principal) {
19146
- return principal.startsWith("usr_") ? principal.slice("usr_".length) : principal;
19147
- }
19148
- function splitCsv(csv) {
19149
- return csv.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
19150
- }
19151
- function parseScopeOverrides(scopes) {
19152
- if (!scopes || scopes.length === 0) return void 0;
19153
- const out = {};
19154
- for (const raw of scopes) {
19155
- const sep = raw.indexOf(":");
19156
- if (sep <= 0 || sep === raw.length - 1) {
19157
- throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
19158
- }
19159
- const ns = raw.slice(0, sep);
19160
- const value = raw.slice(sep + 1);
19161
- const valueError = valueGrammarError(value);
19162
- if (valueError) throw new Error(`--scope value ${valueError}`);
19163
- const key = `scope:${ns}`;
19164
- if (Object.prototype.hasOwnProperty.call(out, key)) {
19165
- throw new Error(`--scope sets namespace '${ns}' more than once \u2014 a scope override takes at most one value per namespace.`);
19166
- }
19167
- out[key] = value;
19168
- }
19169
- if (Object.keys(out).length > 2) {
19170
- throw new Error("--scope allows at most two distinct namespaces.");
19171
- }
19172
- return out;
19173
- }
19174
- function parseJsonObjectFlag(flag, raw) {
19175
- let parsed;
19176
- try {
19177
- parsed = JSON.parse(raw);
19178
- } catch {
19179
- throw new Error(`${flag} is not valid JSON: ${raw}`);
21108
+
21109
+ ${BLUEPRINT_HELP}
21110
+ `);
21111
+ return 1;
19180
21112
  }
19181
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
19182
- throw new Error(`${flag} must be a JSON object (e.g. '{"userId":["<id>"]}').`);
21113
+ if (args.help) {
21114
+ process.stdout.write(`${BLUEPRINT_HELP}
21115
+ `);
21116
+ return 0;
19183
21117
  }
19184
- return parsed;
19185
- }
21118
+ if (!args.sub) {
21119
+ process.stdout.write(`${BLUEPRINT_HELP}
21120
+ `);
21121
+ return 1;
21122
+ }
21123
+ switch (args.sub) {
21124
+ case "init":
21125
+ return runInit(args);
21126
+ case "validate":
21127
+ return runValidate(args);
21128
+ case "plan":
21129
+ return runPlan(args);
21130
+ case "list":
21131
+ return runList();
21132
+ default:
21133
+ process.stderr.write(
21134
+ `Unknown blueprint subcommand: ${args.sub}. Bundled: ${BLUEPRINT_NAMES.join(", ")}
19186
21135
 
19187
- // src/paginate.ts
19188
- var PAGE_LIMIT = 100;
19189
- async function drainPages(fetch2) {
19190
- const out = [];
19191
- let startFrom;
19192
- for (; ; ) {
19193
- const page = await fetch2(startFrom);
19194
- if (page.data) out.push(...page.data);
19195
- const next = page.nextCursor;
19196
- if (!next) break;
19197
- startFrom = next;
21136
+ ${BLUEPRINT_HELP}
21137
+ `
21138
+ );
21139
+ return 1;
19198
21140
  }
19199
- return out;
19200
21141
  }
19201
21142
 
21143
+ // src/program.ts
21144
+ var import_commander = require("commander");
21145
+
19202
21146
  // src/self-principal.ts
19203
21147
  var PRINCIPAL_PREFIX_USER = "usr_";
19204
21148
  function emailFromJwt(token) {
@@ -20216,7 +22160,14 @@ function normalizeClauses(raw) {
20216
22160
  });
20217
22161
  if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
20218
22162
  }
20219
- out.push({ actions: Array.isArray(actions) ? actions.map(String) : [], dataScope, unrecognizedDims });
22163
+ const rawAssignable = obj.assignable_roles ?? obj.assignableRoles;
22164
+ const assignableRoles = Array.isArray(rawAssignable) && rawAssignable.length > 0 ? rawAssignable.map(String) : void 0;
22165
+ out.push({
22166
+ actions: Array.isArray(actions) ? actions.map(String) : [],
22167
+ dataScope,
22168
+ unrecognizedDims,
22169
+ ...assignableRoles ? { assignableRoles } : {}
22170
+ });
20220
22171
  }
20221
22172
  return out;
20222
22173
  }
@@ -20416,7 +22367,10 @@ async function runAccessExplain(opts, deps = {}) {
20416
22367
  dataScope: renderDataScope(c.dataScope),
20417
22368
  // Present only on a dead clause — an unrecognized dim the platform
20418
22369
  // denies (a retired key or a bad scope:<ns>); the matrix omits it.
20419
- ...c.unrecognizedDims.length > 0 ? { grantsNothing: true, unrecognizedDims: c.unrecognizedDims } : {}
22370
+ ...c.unrecognizedDims.length > 0 ? { grantsNothing: true, unrecognizedDims: c.unrecognizedDims } : {},
22371
+ // Reported only when the clause actually carries the restriction. Absent means
22372
+ // default-open (any role composable), which is a different fact from an empty list.
22373
+ ...c.assignableRoles?.length ? { assignableRoles: c.assignableRoles } : {}
20420
22374
  })),
20421
22375
  matrix: rows,
20422
22376
  // Present only when a clause uses `${{ under.self.* }}`: those rows are UNDECIDED
@@ -20442,6 +22396,12 @@ async function runAccessExplain(opts, deps = {}) {
20442
22396
  if (c.unrecognizedDims.length > 0) {
20443
22397
  process.stdout.write(
20444
22398
  ` \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>)
22399
+ `
22400
+ );
22401
+ }
22402
+ if (c.assignableRoles?.length) {
22403
+ process.stdout.write(
22404
+ ` may compose only these roles into a delegated profile: ${c.assignableRoles.join(", ")}
20445
22405
  `
20446
22406
  );
20447
22407
  }
@@ -20507,6 +22467,14 @@ async function runRoleCreate(opts, deps = {}) {
20507
22467
  return 2;
20508
22468
  }
20509
22469
  }
22470
+ let assignableRoles;
22471
+ try {
22472
+ assignableRoles = parseAssignableRoles(opts.assignableRole);
22473
+ } catch (err) {
22474
+ process.stderr.write(` \u2716 ${formatError(err)}
22475
+ `);
22476
+ return 2;
22477
+ }
20510
22478
  return withAuth(opts, deps, async (auth) => {
20511
22479
  const client = auth.client;
20512
22480
  try {
@@ -20519,7 +22487,15 @@ async function runRoleCreate(opts, deps = {}) {
20519
22487
  // data_scope is included only when supplied — the clause wire shape
20520
22488
  // stays byte-identical for unscoped roles.
20521
22489
  scopes: [
20522
- dataScope ? { allowed_actions: actions, data_scope: dataScope } : { allowed_actions: actions }
22490
+ {
22491
+ allowed_actions: actions,
22492
+ // Each included only when supplied — the clause wire shape stays byte-identical for an
22493
+ // unscoped, unrestricted role. `assignable_roles` in particular must be OMITTED rather
22494
+ // than sent empty: the platform reads absent as "no roleId restriction" and rejects
22495
+ // `[]` outright, so the two are not interchangeable.
22496
+ ...dataScope ? { data_scope: dataScope } : {},
22497
+ ...assignableRoles ? { assignable_roles: assignableRoles } : {}
22498
+ }
20523
22499
  ],
20524
22500
  // Included only when supplied — omitting it (rather than sending an
20525
22501
  // empty object) keeps "no assume grant" and "explicitly empty grant"
@@ -20778,6 +22754,36 @@ function parseSelfSignupPolicies(raw) {
20778
22754
  return { signup_type: entry.slice(0, sep), role_id: entry.slice(sep + 1) };
20779
22755
  });
20780
22756
  }
22757
+ var MAX_CAPTURED_CLAIMS2 = 20;
22758
+ var MAX_CAPTURED_CLAIM_NAME_LENGTH2 = 64;
22759
+ function parseCapturedClaims(raw) {
22760
+ if (!raw || raw.length === 0) return void 0;
22761
+ if (raw.length > MAX_CAPTURED_CLAIMS2) {
22762
+ throw new Error(
22763
+ `--captured-claim accepts at most ${MAX_CAPTURED_CLAIMS2} entries; got ${raw.length}.`
22764
+ );
22765
+ }
22766
+ const seen = /* @__PURE__ */ new Set();
22767
+ for (const name of raw) {
22768
+ if (name.trim().length === 0) {
22769
+ throw new Error("--captured-claim must be a non-blank OIDC claim name (e.g. 'phone_number').");
22770
+ }
22771
+ if (name.length > MAX_CAPTURED_CLAIM_NAME_LENGTH2) {
22772
+ throw new Error(
22773
+ `--captured-claim '${name}' is ${name.length} characters, above the ${MAX_CAPTURED_CLAIM_NAME_LENGTH2}-character limit.`
22774
+ );
22775
+ }
22776
+ if (seen.has(name)) {
22777
+ throw new Error(`--captured-claim repeats '${name}' \u2014 each claim name may be given only once.`);
22778
+ }
22779
+ seen.add(name);
22780
+ }
22781
+ return [...raw];
22782
+ }
22783
+ function renderCapturedClaims(claims) {
22784
+ if (!claims || claims.length === 0) return "\u2014";
22785
+ return claims.join(", ");
22786
+ }
20781
22787
  function renderSelfSignup(policies) {
20782
22788
  if (!policies || policies.length === 0) return "\u2014";
20783
22789
  return policies.map((p) => `${p.signup_type} \u2192 ${p.role_id}`).join(", ");
@@ -20794,7 +22800,8 @@ function renderIssuer(i) {
20794
22800
  ["userinfoUri", i.userinfoUri],
20795
22801
  ["status", i.status],
20796
22802
  ["createdAt", i.createdAt],
20797
- ["selfSignup", renderSelfSignup(i.selfSignupPolicies)]
22803
+ ["selfSignup", renderSelfSignup(i.selfSignupPolicies)],
22804
+ ["capturedClaims", renderCapturedClaims(i.capturedClaims)]
20798
22805
  ]);
20799
22806
  }
20800
22807
  async function runIssuersList(opts, deps = {}) {
@@ -20815,7 +22822,8 @@ async function runIssuersList(opts, deps = {}) {
20815
22822
  { header: "issuer", get: (i) => i.issuer },
20816
22823
  { header: "audience", get: (i) => i.audience },
20817
22824
  { header: "status", get: (i) => i.status },
20818
- { header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) }
22825
+ { header: "self-signup", get: (i) => renderSelfSignup(i.selfSignupPolicies) },
22826
+ { header: "captured claims", get: (i) => renderCapturedClaims(i.capturedClaims) }
20819
22827
  ]) + "\n"
20820
22828
  );
20821
22829
  return 0;
@@ -20847,8 +22855,10 @@ ${renderIssuer(issuer)}
20847
22855
  }
20848
22856
  async function runIssuersRegister(opts, deps = {}) {
20849
22857
  let selfSignupPolicies;
22858
+ let capturedClaims;
20850
22859
  try {
20851
22860
  selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
22861
+ capturedClaims = parseCapturedClaims(opts.capturedClaim);
20852
22862
  } catch (err) {
20853
22863
  process.stderr.write(` \u2716 ${formatError(err)}
20854
22864
  `);
@@ -20866,7 +22876,8 @@ async function runIssuersRegister(opts, deps = {}) {
20866
22876
  subClaim: opts.subClaim,
20867
22877
  emailClaim: opts.emailClaim,
20868
22878
  userinfoUri: opts.userinfoUri,
20869
- selfSignupPolicies
22879
+ selfSignupPolicies,
22880
+ capturedClaims
20870
22881
  });
20871
22882
  if (opts.json) {
20872
22883
  process.stdout.write(`${formatJson(issuer)}
@@ -20887,8 +22898,10 @@ ${renderIssuer(issuer)}
20887
22898
  }
20888
22899
  async function runIssuersUpdate(opts, deps = {}) {
20889
22900
  let selfSignupPolicies;
22901
+ let capturedClaims;
20890
22902
  try {
20891
22903
  selfSignupPolicies = parseSelfSignupPolicies(opts.selfSignupPolicy);
22904
+ capturedClaims = parseCapturedClaims(opts.capturedClaim);
20892
22905
  } catch (err) {
20893
22906
  process.stderr.write(` \u2716 ${formatError(err)}
20894
22907
  `);
@@ -20900,6 +22913,12 @@ async function runIssuersUpdate(opts, deps = {}) {
20900
22913
  );
20901
22914
  return 2;
20902
22915
  }
22916
+ if (opts.clearCapturedClaims && capturedClaims && capturedClaims.length > 0) {
22917
+ process.stderr.write(
22918
+ " \u2716 --clear-captured-claims and --captured-claim are mutually exclusive.\n"
22919
+ );
22920
+ return 2;
22921
+ }
20903
22922
  return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
20904
22923
  const client = auth.client;
20905
22924
  try {
@@ -20909,7 +22928,8 @@ async function runIssuersUpdate(opts, deps = {}) {
20909
22928
  emailClaim: opts.emailClaim,
20910
22929
  userinfoUri: opts.userinfoUri,
20911
22930
  status: opts.status,
20912
- selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies
22931
+ selfSignupPolicies: opts.clearSelfSignupPolicies ? [] : selfSignupPolicies,
22932
+ capturedClaims: opts.clearCapturedClaims ? [] : capturedClaims
20913
22933
  });
20914
22934
  if (opts.json) {
20915
22935
  process.stdout.write(`${formatJson(issuer)}
@@ -20968,41 +22988,264 @@ async function runIssuersDelete(opts, deps = {}) {
20968
22988
  try {
20969
22989
  await client.auth.deleteIssuer({ issuerId });
20970
22990
  if (opts.json) {
20971
- process.stdout.write(`${formatJson({ issuerId, deleted: true })}
22991
+ process.stdout.write(`${formatJson({ issuerId, deleted: true })}
22992
+ `);
22993
+ return 0;
22994
+ }
22995
+ process.stdout.write(`
22996
+ \u2714 issuer '${issuerId}' deregistered from '${context}'
22997
+ `);
22998
+ return 0;
22999
+ } catch (err) {
23000
+ process.stderr.write(formatCliError(err, { action: "issuers delete failed" }));
23001
+ return 1;
23002
+ }
23003
+ });
23004
+ }
23005
+
23006
+ // src/commands/namespace.ts
23007
+ function renderNamespace(n) {
23008
+ return formatKeyValues([
23009
+ ["namespace", n.namespace],
23010
+ ["contextId", n.contextId ?? "(tenant-wide)"],
23011
+ ["entityBacked", n.entityBacked === void 0 ? void 0 : String(n.entityBacked)],
23012
+ ["reserved", n.reserved === void 0 ? void 0 : String(n.reserved)],
23013
+ ["specificityRank", n.specificityRank === void 0 ? void 0 : String(n.specificityRank)],
23014
+ ["defaultSchemaId", n.defaultSchemaId],
23015
+ ["createdAt", n.createdAt]
23016
+ ]);
23017
+ }
23018
+ async function runNamespaceList(opts, deps = {}) {
23019
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
23020
+ const client = auth.client;
23021
+ try {
23022
+ const rows = await drainPages(
23023
+ (startFrom) => client.identity.listNamespaces({
23024
+ limit: PAGE_LIMIT,
23025
+ ...opts.context ? { contextId: opts.context } : {},
23026
+ ...startFrom ? { startFrom } : {}
23027
+ })
23028
+ );
23029
+ if (opts.json) {
23030
+ process.stdout.write(`${formatJson(rows)}
23031
+ `);
23032
+ return 0;
23033
+ }
23034
+ process.stdout.write(
23035
+ "\n" + formatTable(rows, [
23036
+ { header: "namespace", get: (n) => n.namespace },
23037
+ { header: "contextId", get: (n) => n.contextId ?? "(tenant-wide)" },
23038
+ {
23039
+ header: "entityBacked",
23040
+ get: (n) => n.entityBacked === void 0 ? void 0 : String(n.entityBacked)
23041
+ },
23042
+ {
23043
+ header: "specificityRank",
23044
+ get: (n) => n.specificityRank === void 0 ? void 0 : String(n.specificityRank)
23045
+ }
23046
+ ]) + "\n"
23047
+ );
23048
+ return 0;
23049
+ } catch (err) {
23050
+ process.stderr.write(formatCliError(err, { action: "namespace list failed" }));
23051
+ return 1;
23052
+ }
23053
+ });
23054
+ }
23055
+ async function runNamespaceGet(opts, deps = {}) {
23056
+ return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
23057
+ const client = auth.client;
23058
+ try {
23059
+ const ns = await client.identity.getNamespace({
23060
+ namespace: opts.namespace,
23061
+ ...opts.context ? { contextId: opts.context } : {}
23062
+ });
23063
+ if (opts.json) {
23064
+ process.stdout.write(`${formatJson(ns)}
23065
+ `);
23066
+ return 0;
23067
+ }
23068
+ process.stdout.write(`
23069
+ ${renderNamespace(ns)}
23070
+ `);
23071
+ return 0;
23072
+ } catch (err) {
23073
+ process.stderr.write(formatCliError(err, { action: "namespace get failed" }));
23074
+ return 1;
23075
+ }
23076
+ });
23077
+ }
23078
+ async function runNamespaceRegister(opts, deps = {}) {
23079
+ return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
23080
+ const client = auth.client;
23081
+ try {
23082
+ const ns = await client.identity.registerNamespace({
23083
+ contextId: opts.context,
23084
+ body: {
23085
+ namespace: opts.namespace,
23086
+ specificityRank: opts.specificityRank,
23087
+ entityBacked: opts.entityBacked,
23088
+ defaultSchemaId: opts.defaultSchemaId
23089
+ }
23090
+ });
23091
+ if (opts.json) {
23092
+ process.stdout.write(`${formatJson(ns)}
23093
+ `);
23094
+ return 0;
23095
+ }
23096
+ process.stdout.write(`
23097
+ \u2714 namespace '${opts.namespace}' registered
23098
+ ${renderNamespace(ns)}
23099
+ `);
23100
+ return 0;
23101
+ } catch (err) {
23102
+ process.stderr.write(formatCliError(err, { action: "namespace register failed" }));
23103
+ return 1;
23104
+ }
23105
+ });
23106
+ }
23107
+
23108
+ // src/commands/scripts.ts
23109
+ var import_promises4 = require("fs/promises");
23110
+ var readline5 = __toESM(require("readline/promises"));
23111
+ var SOURCE_MAX_BYTES = 3e5;
23112
+ var INPUT_CONTRACT_MAX_BYTES = 5e4;
23113
+ var utf8Bytes2 = (s) => new TextEncoder().encode(s).length;
23114
+ var SCRIPT_NAME_RE2 = /^[A-Za-z0-9_-]{1,64}$/;
23115
+ function renderScript(s, opts = {}) {
23116
+ return formatKeyValues([
23117
+ ["id", s.id],
23118
+ ["name", s.name],
23119
+ ["scriptVersion", s.scriptVersion === void 0 ? void 0 : String(s.scriptVersion)],
23120
+ ["declaredInputContract", s.declaredInputContract],
23121
+ ["createdAt", s.createdAt],
23122
+ // Full source last, and only where it was asked for — it can be 300 KB.
23123
+ ...opts.source && s.source !== void 0 ? [["source", s.source]] : []
23124
+ ]);
23125
+ }
23126
+ async function runScriptsPush(opts, deps = {}) {
23127
+ if (!SCRIPT_NAME_RE2.test(opts.name)) {
23128
+ process.stderr.write(
23129
+ ` \u2716 '${opts.name}' is not a valid script name \u2014 use 1-64 characters of letters, digits, underscores ('_') and hyphens ('-').
23130
+ `
23131
+ );
23132
+ return 2;
23133
+ }
23134
+ if (opts.inputContract !== void 0 && opts.inputContractFile !== void 0) {
23135
+ process.stderr.write(" \u2716 --input-contract and --input-contract-file are mutually exclusive.\n");
23136
+ return 2;
23137
+ }
23138
+ let source;
23139
+ try {
23140
+ source = await (0, import_promises4.readFile)(opts.sourceFile, "utf8");
23141
+ } catch (err) {
23142
+ process.stderr.write(` \u2716 could not read --source-file '${opts.sourceFile}': ${errText(err)}
23143
+ `);
23144
+ return 2;
23145
+ }
23146
+ if (source.trim().length === 0) {
23147
+ process.stderr.write(` \u2716 --source-file '${opts.sourceFile}' is empty \u2014 'source' must not be blank.
23148
+ `);
23149
+ return 2;
23150
+ }
23151
+ if (opts.inputContract !== void 0 && opts.inputContract.trim().length === 0) {
23152
+ process.stderr.write(
23153
+ ` \u2716 --input-contract is blank. Omit the flag to store no input contract at all \u2014 an empty one is not a contract.
23154
+ `
23155
+ );
23156
+ return 2;
23157
+ }
23158
+ let declaredInputContract = opts.inputContract;
23159
+ if (opts.inputContractFile !== void 0) {
23160
+ try {
23161
+ declaredInputContract = await (0, import_promises4.readFile)(opts.inputContractFile, "utf8");
23162
+ } catch (err) {
23163
+ process.stderr.write(
23164
+ ` \u2716 could not read --input-contract-file '${opts.inputContractFile}': ${errText(err)}
23165
+ `
23166
+ );
23167
+ return 2;
23168
+ }
23169
+ if (declaredInputContract.trim().length === 0) {
23170
+ process.stderr.write(
23171
+ ` \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.
23172
+ `
23173
+ );
23174
+ return 2;
23175
+ }
23176
+ }
23177
+ const sourceBytes = utf8Bytes2(source);
23178
+ if (sourceBytes > SOURCE_MAX_BYTES) {
23179
+ process.stderr.write(
23180
+ ` \u2716 source is ${sourceBytes.toLocaleString("en-US")} UTF-8 bytes, over the ${SOURCE_MAX_BYTES.toLocaleString("en-US")}-byte limit.
23181
+ `
23182
+ );
23183
+ return 2;
23184
+ }
23185
+ if (declaredInputContract !== void 0 && utf8Bytes2(declaredInputContract) > INPUT_CONTRACT_MAX_BYTES) {
23186
+ process.stderr.write(
23187
+ ` \u2716 input contract is ${utf8Bytes2(declaredInputContract).toLocaleString("en-US")} UTF-8 bytes, over the ${INPUT_CONTRACT_MAX_BYTES.toLocaleString("en-US")}-byte limit.
23188
+ `
23189
+ );
23190
+ return 2;
23191
+ }
23192
+ return withAuth(opts, deps, async (auth) => {
23193
+ const client = auth.client;
23194
+ try {
23195
+ if (opts.ifChanged) {
23196
+ const latest = await readLatestScriptVersion(
23197
+ client,
23198
+ opts.name
23199
+ );
23200
+ if (latest !== void 0 && scriptMatches(latest, { source, declaredInputContract })) {
23201
+ if (opts.json) {
23202
+ process.stdout.write(`${formatJson({ ...latest, pushed: false })}
23203
+ `);
23204
+ return 0;
23205
+ }
23206
+ process.stdout.write(
23207
+ `
23208
+ = script '${opts.name}' already matches at version ${latest.scriptVersion} \u2014 nothing pushed.
23209
+ `
23210
+ );
23211
+ return 0;
23212
+ }
23213
+ }
23214
+ const pushed = await client.scripts.createScript({
23215
+ name: opts.name,
23216
+ source,
23217
+ declaredInputContract
23218
+ });
23219
+ if (opts.json) {
23220
+ process.stdout.write(`${formatJson({ ...pushed, pushed: true })}
20972
23221
  `);
20973
23222
  return 0;
20974
23223
  }
20975
- process.stdout.write(`
20976
- \u2714 issuer '${issuerId}' deregistered from '${context}'
20977
- `);
23224
+ process.stdout.write(
23225
+ `
23226
+ \u2714 pushed script '${opts.name}' version ${pushed.scriptVersion} (${sourceBytes.toLocaleString("en-US")} bytes) to '${opts.context}'
23227
+ ${renderScript(pushed)}
23228
+ A trigger rule reaches this with scriptRef { name: '${opts.name}', version: 'latest' }.
23229
+ Pinning version '${pushed.scriptVersion}' instead keeps that rule on exactly this one \u2014
23230
+ but only for a script pushed this way. A blueprint that SHIPS a script requires 'latest'
23231
+ for it, since the blueprint cannot know which number its source will land on.
23232
+ `
23233
+ );
20978
23234
  return 0;
20979
23235
  } catch (err) {
20980
- process.stderr.write(formatCliError(err, { action: "issuers delete failed" }));
23236
+ process.stderr.write(formatCliError(err, { action: "scripts push failed" }));
20981
23237
  return 1;
20982
23238
  }
20983
23239
  });
20984
23240
  }
20985
-
20986
- // src/commands/namespace.ts
20987
- function renderNamespace(n) {
20988
- return formatKeyValues([
20989
- ["namespace", n.namespace],
20990
- ["contextId", n.contextId ?? "(tenant-wide)"],
20991
- ["entityBacked", n.entityBacked === void 0 ? void 0 : String(n.entityBacked)],
20992
- ["reserved", n.reserved === void 0 ? void 0 : String(n.reserved)],
20993
- ["specificityRank", n.specificityRank === void 0 ? void 0 : String(n.specificityRank)],
20994
- ["defaultSchemaId", n.defaultSchemaId],
20995
- ["createdAt", n.createdAt]
20996
- ]);
20997
- }
20998
- async function runNamespaceList(opts, deps = {}) {
20999
- return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
23241
+ async function runScriptsList(opts, deps = {}) {
23242
+ return withAuth(opts, deps, async (auth) => {
21000
23243
  const client = auth.client;
21001
23244
  try {
21002
23245
  const rows = await drainPages(
21003
- (startFrom) => client.identity.listNamespaces({
23246
+ (startFrom) => client.scripts.listScripts({
21004
23247
  limit: PAGE_LIMIT,
21005
- ...opts.context ? { contextId: opts.context } : {},
23248
+ ...opts.name ? { name: opts.name } : {},
21006
23249
  ...startFrom ? { startFrom } : {}
21007
23250
  })
21008
23251
  );
@@ -21011,79 +23254,113 @@ async function runNamespaceList(opts, deps = {}) {
21011
23254
  `);
21012
23255
  return 0;
21013
23256
  }
23257
+ if (rows.length === 0) {
23258
+ process.stdout.write(
23259
+ opts.name ? `
23260
+ No versions of script '${opts.name}' in '${opts.context}'.
23261
+ ` : `
23262
+ No scripts in '${opts.context}'.
23263
+ `
23264
+ );
23265
+ return 0;
23266
+ }
21014
23267
  process.stdout.write(
21015
23268
  "\n" + formatTable(rows, [
21016
- { header: "namespace", get: (n) => n.namespace },
21017
- { header: "contextId", get: (n) => n.contextId ?? "(tenant-wide)" },
21018
- {
21019
- header: "entityBacked",
21020
- get: (n) => n.entityBacked === void 0 ? void 0 : String(n.entityBacked)
21021
- },
21022
- {
21023
- header: "specificityRank",
21024
- get: (n) => n.specificityRank === void 0 ? void 0 : String(n.specificityRank)
21025
- }
23269
+ { header: "name", get: (s) => s.name },
23270
+ { header: "version", get: (s) => s.scriptVersion === void 0 ? void 0 : String(s.scriptVersion) },
23271
+ { header: "id", get: (s) => s.id },
23272
+ { header: "bytes", get: (s) => s.source === void 0 ? void 0 : String(utf8Bytes2(s.source)) },
23273
+ { header: "createdAt", get: (s) => s.createdAt }
21026
23274
  ]) + "\n"
21027
23275
  );
21028
23276
  return 0;
21029
23277
  } catch (err) {
21030
- process.stderr.write(formatCliError(err, { action: "namespace list failed" }));
23278
+ process.stderr.write(formatCliError(err, { action: "scripts list failed" }));
21031
23279
  return 1;
21032
23280
  }
21033
23281
  });
21034
23282
  }
21035
- async function runNamespaceGet(opts, deps = {}) {
21036
- return withAuth({ ...opts, context: opts.context }, deps, async (auth) => {
23283
+ async function runScriptsGet(opts, deps = {}) {
23284
+ return withAuth(opts, deps, async (auth) => {
21037
23285
  const client = auth.client;
21038
23286
  try {
21039
- const ns = await client.identity.getNamespace({
21040
- namespace: opts.namespace,
21041
- ...opts.context ? { contextId: opts.context } : {}
21042
- });
23287
+ const script = await client.scripts.getScript({ id: opts.id });
21043
23288
  if (opts.json) {
21044
- process.stdout.write(`${formatJson(ns)}
23289
+ process.stdout.write(`${formatJson(script)}
21045
23290
  `);
21046
23291
  return 0;
21047
23292
  }
21048
23293
  process.stdout.write(`
21049
- ${renderNamespace(ns)}
23294
+ ${renderScript(script, { source: true })}
21050
23295
  `);
21051
23296
  return 0;
21052
23297
  } catch (err) {
21053
- process.stderr.write(formatCliError(err, { action: "namespace get failed" }));
23298
+ process.stderr.write(formatCliError(err, { action: "scripts get failed" }));
21054
23299
  return 1;
21055
23300
  }
21056
23301
  });
21057
23302
  }
21058
- async function runNamespaceRegister(opts, deps = {}) {
21059
- return withAuth({ ...opts, provisioning: true, context: opts.context }, deps, async (auth) => {
23303
+ async function runScriptsDelete(opts, deps = {}) {
23304
+ process.stderr.write(
23305
+ `
23306
+ \u26A0 Deleting script version '${opts.id}' is permanent \u2014 the stored source is the only copy.
23307
+ Other versions of the same name are unaffected.
23308
+ Nothing checks whether a trigger rule still references this version. A rule pinned to it
23309
+ will fail on every firing; a rule on 'latest' will silently fall back to the previous
23310
+ version and keep running older code. This CLI cannot list trigger rules, so confirm
23311
+ against the API or the portal before removing a version a rule may still use.
23312
+ And if this is the name's LATEST version, its identity is REUSED: the next push of that
23313
+ name recomputes the same version number and the same id, so anything holding a pinned
23314
+ '${opts.id}' or a name@version reference will silently resolve to different source.
23315
+
23316
+ `
23317
+ );
23318
+ if (!opts.force) {
23319
+ const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
23320
+ if (!isTty) {
23321
+ process.stderr.write(
23322
+ ` \u2716 scripts delete is irreversible and needs an interactive confirmation.
23323
+ Re-run with --force to delete '${opts.id}' without a prompt.
23324
+ `
23325
+ );
23326
+ return 2;
23327
+ }
23328
+ const promptCtx = deps.createPrompt ? deps.createPrompt() : (() => {
23329
+ const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
23330
+ return { prompt: (q) => rl.question(q), close: () => rl.close() };
23331
+ })();
23332
+ try {
23333
+ const typed = await promptCtx.prompt(` Type the version id ('${opts.id}') to confirm: `);
23334
+ if (typed.trim() !== opts.id) {
23335
+ process.stderr.write(` \u2716 Confirmation did not match \u2014 nothing deleted.
23336
+ `);
23337
+ return 1;
23338
+ }
23339
+ } finally {
23340
+ promptCtx.close();
23341
+ }
23342
+ }
23343
+ return withAuth(opts, deps, async (auth) => {
21060
23344
  const client = auth.client;
21061
23345
  try {
21062
- const ns = await client.identity.registerNamespace({
21063
- contextId: opts.context,
21064
- body: {
21065
- namespace: opts.namespace,
21066
- specificityRank: opts.specificityRank,
21067
- entityBacked: opts.entityBacked,
21068
- defaultSchemaId: opts.defaultSchemaId
21069
- }
21070
- });
23346
+ await client.scripts.deleteScript({ id: opts.id });
21071
23347
  if (opts.json) {
21072
- process.stdout.write(`${formatJson(ns)}
23348
+ process.stdout.write(`${formatJson({ id: opts.id, deleted: true })}
21073
23349
  `);
21074
23350
  return 0;
21075
23351
  }
21076
- process.stdout.write(`
21077
- \u2714 namespace '${opts.namespace}' registered
21078
- ${renderNamespace(ns)}
23352
+ process.stdout.write(` \u2714 Deleted script version '${opts.id}' from '${opts.context}'.
21079
23353
  `);
21080
23354
  return 0;
21081
23355
  } catch (err) {
21082
- process.stderr.write(formatCliError(err, { action: "namespace register failed" }));
23356
+ process.stderr.write(formatCliError(err, { action: "scripts delete failed" }));
21083
23357
  return 1;
21084
23358
  }
21085
23359
  });
21086
23360
  }
23361
+ function errText(err) {
23362
+ return err instanceof Error ? err.message : String(err);
23363
+ }
21087
23364
 
21088
23365
  // src/commands/assume.ts
21089
23366
  function parseAssumeScopes(raw) {
@@ -21106,11 +23383,20 @@ function parseAssumeScopes(raw) {
21106
23383
  return out;
21107
23384
  }
21108
23385
  function renderAssumeToken(t) {
21109
- return formatKeyValues([
23386
+ const identity = t.resolvedScope?.identity;
23387
+ const allowed = t.resolvedScope?.allowedActions;
23388
+ const rows = [
21110
23389
  ["access_token", t.access_token],
21111
23390
  ["token_type", t.token_type],
21112
23391
  ["expires_in", t.expires_in === void 0 ? void 0 : `${t.expires_in}s`]
21113
- ]);
23392
+ ];
23393
+ if (identity && Object.keys(identity).length > 0) {
23394
+ rows.push(["identity", Object.entries(identity).map(([k, v]) => `${k}=${v}`).join(", ")]);
23395
+ }
23396
+ if (allowed?.length) {
23397
+ rows.push(["allowedActions", allowed.join(", ")]);
23398
+ }
23399
+ return formatKeyValues(rows);
21114
23400
  }
21115
23401
  async function runAssume(opts, deps = {}) {
21116
23402
  if (!opts.scope || opts.scope.length === 0) {
@@ -21295,12 +23581,14 @@ var import_node_crypto4 = require("crypto");
21295
23581
 
21296
23582
  // src/harness.ts
21297
23583
  var errMsg = (e) => e instanceof Error ? e.message : String(e);
21298
- var isNotFound = (e) => typeof e === "object" && e !== null && e.statusCode === 404;
23584
+ var isNotFound2 = (e) => typeof e === "object" && e !== null && e.statusCode === 404;
21299
23585
  var SNAPSHOT_PAGE_SIZE = 100;
21300
23586
  async function captureSnapshot(client, blueprint) {
21301
23587
  const existingSchemaTypes = /* @__PURE__ */ new Set();
21302
23588
  const existingProfilePrincipals = /* @__PURE__ */ new Set();
21303
23589
  const existingRoleIds = /* @__PURE__ */ new Set();
23590
+ const existingTriggerNames = /* @__PURE__ */ new Set();
23591
+ let triggerSnapshotFailed = false;
21304
23592
  let failed = false;
21305
23593
  try {
21306
23594
  let cursor;
@@ -21312,7 +23600,7 @@ async function captureSnapshot(client, blueprint) {
21312
23600
  cursor = next;
21313
23601
  }
21314
23602
  } catch (e) {
21315
- if (!isNotFound(e)) failed = true;
23603
+ if (!isNotFound2(e)) failed = true;
21316
23604
  }
21317
23605
  try {
21318
23606
  let startFrom;
@@ -21328,7 +23616,7 @@ async function captureSnapshot(client, blueprint) {
21328
23616
  startFrom = next;
21329
23617
  }
21330
23618
  } catch (e) {
21331
- if (!isNotFound(e)) failed = true;
23619
+ if (!isNotFound2(e)) failed = true;
21332
23620
  }
21333
23621
  try {
21334
23622
  let startFrom;
@@ -21344,9 +23632,23 @@ async function captureSnapshot(client, blueprint) {
21344
23632
  startFrom = next;
21345
23633
  }
21346
23634
  } catch (e) {
21347
- if (!isNotFound(e)) failed = true;
23635
+ if (!isNotFound2(e)) failed = true;
23636
+ }
23637
+ if (Object.keys(blueprint.triggers ?? {}).length > 0) {
23638
+ try {
23639
+ let startFrom;
23640
+ for (; ; ) {
23641
+ const page = await client.triggers.listTriggers({ limit: SNAPSHOT_PAGE_SIZE, startFrom });
23642
+ for (const t of page.data ?? []) if (t.name) existingTriggerNames.add(t.name);
23643
+ const next = page.nextCursor;
23644
+ if (!next) break;
23645
+ startFrom = next;
23646
+ }
23647
+ } catch (e) {
23648
+ if (!isNotFound2(e)) triggerSnapshotFailed = true;
23649
+ }
21348
23650
  }
21349
- return { existingSchemaTypes, existingProfilePrincipals, existingRoleIds, failed };
23651
+ return { existingSchemaTypes, existingProfilePrincipals, existingRoleIds, existingTriggerNames, triggerSnapshotFailed, failed };
21350
23652
  }
21351
23653
  async function captureIssuerSnapshot(bootstrapClient) {
21352
23654
  const existingIssuerIds = /* @__PURE__ */ new Set();
@@ -21542,6 +23844,97 @@ async function runTeardown(client, bootstrapClient, blueprint, machineId, snapsh
21542
23844
  steps.push({ step: `delete role ${roleId}`, ok: false, error: errMsg(e) });
21543
23845
  }
21544
23846
  }
23847
+ const declaredTriggerNames = Object.keys(blueprint.triggers ?? {});
23848
+ let allDeclaredTriggersDeleted = true;
23849
+ if (declaredTriggerNames.length > 0) {
23850
+ let live = [];
23851
+ let listFailed = false;
23852
+ try {
23853
+ let startFrom;
23854
+ for (; ; ) {
23855
+ const page = await client.triggers.listTriggers({ limit: SNAPSHOT_PAGE_SIZE, startFrom });
23856
+ live = live.concat(page.data ?? []);
23857
+ const next = page.nextCursor;
23858
+ if (!next) break;
23859
+ startFrom = next;
23860
+ }
23861
+ } catch (e) {
23862
+ listFailed = true;
23863
+ steps.push({ step: "delete triggers", ok: false, error: `could not list triggers: ${errMsg(e)}` });
23864
+ allDeclaredTriggersDeleted = false;
23865
+ }
23866
+ if (!listFailed) {
23867
+ for (const name of declaredTriggerNames) {
23868
+ if (snapshot.failed || snapshot.triggerSnapshotFailed) {
23869
+ steps.push({ step: `delete trigger ${name}`, ok: true, skipped: true, error: "skipped \u2014 pre-apply snapshot failed; cannot prove ownership" });
23870
+ allDeclaredTriggersDeleted = false;
23871
+ continue;
23872
+ }
23873
+ if (snapshot.existingTriggerNames.has(name)) {
23874
+ steps.push({ step: `delete trigger ${name}`, ok: true, skipped: true, error: "skipped \u2014 pre-existed this run" });
23875
+ allDeclaredTriggersDeleted = false;
23876
+ continue;
23877
+ }
23878
+ const row = live.find((t) => t.name === name);
23879
+ if (!row?.id) {
23880
+ steps.push({ step: `delete trigger ${name}`, ok: true, skipped: true, error: "skipped \u2014 not found (apply may not have reached it)" });
23881
+ continue;
23882
+ }
23883
+ if (row.provisionedBy !== blueprint.name) {
23884
+ steps.push({
23885
+ step: `delete trigger ${name}`,
23886
+ ok: true,
23887
+ skipped: true,
23888
+ error: `skipped \u2014 provisionedBy is ${row.provisionedBy ? `'${row.provisionedBy}'` : "unset"}, not this blueprint`
23889
+ });
23890
+ allDeclaredTriggersDeleted = false;
23891
+ continue;
23892
+ }
23893
+ try {
23894
+ await client.triggers.deleteTrigger({ id: row.id });
23895
+ steps.push({ step: `delete trigger ${name}`, ok: true });
23896
+ } catch (e) {
23897
+ steps.push({ step: `delete trigger ${name}`, ok: false, error: errMsg(e) });
23898
+ allDeclaredTriggersDeleted = false;
23899
+ continue;
23900
+ }
23901
+ if (row.principalId) {
23902
+ try {
23903
+ await client.auth.deleteAccessProfile({ contextId: blueprint.contextId, principalId: row.principalId });
23904
+ steps.push({ step: `delete trigger ${name} profile`, ok: true });
23905
+ } catch (e) {
23906
+ steps.push({ step: `delete trigger ${name} profile`, ok: false, error: errMsg(e) });
23907
+ }
23908
+ }
23909
+ }
23910
+ }
23911
+ }
23912
+ for (const script of result.scriptActions) {
23913
+ if (!allDeclaredTriggersDeleted) {
23914
+ steps.push({
23915
+ step: `delete script ${script.name} v${script.scriptVersion}`,
23916
+ ok: true,
23917
+ skipped: true,
23918
+ 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"
23919
+ });
23920
+ continue;
23921
+ }
23922
+ if (script.action === "unchanged") {
23923
+ steps.push({
23924
+ step: `delete script ${script.name} v${script.scriptVersion}`,
23925
+ ok: true,
23926
+ skipped: true,
23927
+ error: "skipped \u2014 this run pushed no version (the stored source already matched)"
23928
+ });
23929
+ continue;
23930
+ }
23931
+ try {
23932
+ await client.scripts.deleteScript({ id: script.id });
23933
+ steps.push({ step: `delete script ${script.name} v${script.scriptVersion}`, ok: true });
23934
+ } catch (e) {
23935
+ steps.push({ step: `delete script ${script.name} v${script.scriptVersion}`, ok: false, error: errMsg(e) });
23936
+ }
23937
+ }
21545
23938
  const toDelete = [];
21546
23939
  for (const [typeName, schemaId] of Object.entries(result.schemaIds)) {
21547
23940
  if (snapshot.failed) {
@@ -21628,7 +24021,9 @@ async function runBlueprintHarness(client, input, deps) {
21628
24021
  existingSchemaTypes: /* @__PURE__ */ new Set(),
21629
24022
  existingProfilePrincipals: /* @__PURE__ */ new Set(),
21630
24023
  existingRoleIds: /* @__PURE__ */ new Set(),
21631
- failed: !isNotFound(e)
24024
+ existingTriggerNames: /* @__PURE__ */ new Set(),
24025
+ triggerSnapshotFailed: !isNotFound2(e),
24026
+ failed: !isNotFound2(e)
21632
24027
  };
21633
24028
  }
21634
24029
  } else {
@@ -21641,7 +24036,7 @@ async function runBlueprintHarness(client, input, deps) {
21641
24036
  try {
21642
24037
  issuerSnapshot = await captureIssuerSnapshot(await remintBootstrapForContext(blueprint.contextId));
21643
24038
  } catch (e) {
21644
- issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound(e) };
24039
+ issuerSnapshot = { existingIssuerIds: /* @__PURE__ */ new Set(), failed: !isNotFound2(e) };
21645
24040
  }
21646
24041
  } else {
21647
24042
  issuerSnapshot = await captureIssuerSnapshot(client);
@@ -21664,7 +24059,17 @@ async function runBlueprintHarness(client, input, deps) {
21664
24059
  log,
21665
24060
  provisioned,
21666
24061
  remintForContext,
21667
- remintBootstrapForContext
24062
+ remintBootstrapForContext,
24063
+ // CREATED-ONLY, and this is the seam that keeps it true for triggers. The loader's step 5d
24064
+ // reconciles triggers by ABSENCE — it deletes rules this blueprint provisioned and no longer
24065
+ // declares. That is right for a real bootstrap run, which owns its context; it is wrong here.
24066
+ // This harness may run against a shared tenant, and its whole safety contract is that a run
24067
+ // touches only what it CREATED: `captureSnapshot` protects pre-existing schemas, profiles,
24068
+ // roles and issuers, and `runTeardown` skips them. There is no trigger leg in either, so a
24069
+ // prune would delete a pre-existing trigger — and its access profile — with nothing
24070
+ // recording that it happened and nothing able to restore it. Opt out rather than grow the
24071
+ // snapshot a leg teardown could not honour anyway.
24072
+ pruneTriggers: false
21668
24073
  });
21669
24074
  log.debug({ component: "harness", contextId: result.contextId, keyName: result.keyName }, "applied");
21670
24075
  assertions = await runAssertions(
@@ -21766,14 +24171,14 @@ async function runBlueprintTest(fileOrName, opts, deps = {}) {
21766
24171
  if (err instanceof ScopeGateError) {
21767
24172
  process.stderr.write(` \u2716 Scope gate refused blueprint:
21768
24173
  `);
21769
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
24174
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
21770
24175
  `);
21771
24176
  return 2;
21772
24177
  }
21773
24178
  if (err instanceof CapabilityGateError) {
21774
24179
  process.stderr.write(` \u2716 Capability gate refused blueprint:
21775
24180
  `);
21776
- for (const r of err.rejected) process.stderr.write(` \u2022 ${r.action}: ${r.reason}
24181
+ for (const r of err.rejected) process.stderr.write(` \u2022 ${formatRejection(r)}
21777
24182
  `);
21778
24183
  return 2;
21779
24184
  }
@@ -22025,9 +24430,9 @@ function emitJoined(opts) {
22025
24430
 
22026
24431
  // src/build-info.ts
22027
24432
  var BUILD_INFO = {
22028
- cli: true ? "0.20.0" : "dev",
22029
- sdk: true ? "0.42.0" : "dev",
22030
- blueprints: true ? "0.17.0" : "dev"
24433
+ cli: true ? "0.21.0" : "dev",
24434
+ sdk: true ? "0.43.0" : "dev",
24435
+ blueprints: true ? "0.18.0" : "dev"
22031
24436
  };
22032
24437
  function formatBuildInfo() {
22033
24438
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;
@@ -22038,6 +24443,9 @@ var KEY_FORMATS = ["human", "raw", "env", "json"];
22038
24443
  function collect(value, previous) {
22039
24444
  return [...previous, value];
22040
24445
  }
24446
+ function collectOptional(value, previous) {
24447
+ return [...previous ?? [], value];
24448
+ }
22041
24449
  function addAuthOptions(cmd) {
22042
24450
  return cmd.addOption(
22043
24451
  new import_commander.Option("--env <env>", "target environment").choices(["staging", "production"]).default("production").hideHelp()
@@ -22109,6 +24517,7 @@ function buildProgram(deps = {}) {
22109
24517
  registerContextCommands(program, deps);
22110
24518
  registerIssuersCommands(program, deps);
22111
24519
  registerNamespaceCommands(program, deps);
24520
+ registerScriptsCommands(program, deps);
22112
24521
  registerAssumeCommand(program, deps);
22113
24522
  registerJoinCommand(program, deps);
22114
24523
  registerKeyringCommands(program);
@@ -22284,6 +24693,10 @@ function registerIssuersCommands(program, deps) {
22284
24693
  "opt-in self-service signup entry, repeatable (e.g. default:member) \u2014 no entry may target an elevated role",
22285
24694
  collect,
22286
24695
  []
24696
+ ).option(
24697
+ "--captured-claim <name>",
24698
+ "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",
24699
+ collectOptional
22287
24700
  ).option("--tenant <tenant>", "tenant the context lives in: 'test' or 'live' (default live)");
22288
24701
  addCommonAuthOptions(register).action(
22289
24702
  async (flags) => {
@@ -22301,6 +24714,7 @@ function registerIssuersCommands(program, deps) {
22301
24714
  emailClaim: flags.emailClaim,
22302
24715
  userinfoUri: flags.userinfoUri,
22303
24716
  selfSignupPolicy: flags.selfSignupPolicy,
24717
+ capturedClaim: flags.capturedClaim,
22304
24718
  tenant
22305
24719
  },
22306
24720
  deps
@@ -22313,7 +24727,11 @@ function registerIssuersCommands(program, deps) {
22313
24727
  "replace self-signup policies, repeatable (e.g. default:member) \u2014 omit entirely to leave unchanged",
22314
24728
  collect,
22315
24729
  []
22316
- ).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)");
24730
+ ).option("--clear-self-signup-policies", "disable self-signup entirely (sends an empty list; XOR --self-signup-policy)").option(
24731
+ "--captured-claim <name>",
24732
+ "replace the additional OIDC claims captured beyond the email claim, repeatable \u2014 omit entirely to leave unchanged",
24733
+ collectOptional
24734
+ ).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)");
22317
24735
  addCommonAuthOptions(update).action(
22318
24736
  async (issuerId, flags) => {
22319
24737
  const tenant = parseTenantFlag(flags.tenant);
@@ -22329,6 +24747,8 @@ function registerIssuersCommands(program, deps) {
22329
24747
  status: flags.status,
22330
24748
  selfSignupPolicy: flags.selfSignupPolicy,
22331
24749
  clearSelfSignupPolicies: Boolean(flags.clearSelfSignupPolicies),
24750
+ capturedClaim: flags.capturedClaim,
24751
+ clearCapturedClaims: Boolean(flags.clearCapturedClaims),
22332
24752
  tenant
22333
24753
  },
22334
24754
  deps
@@ -22390,6 +24810,68 @@ function registerNamespaceCommands(program, deps) {
22390
24810
  }
22391
24811
  );
22392
24812
  }
24813
+ function registerScriptsCommands(program, deps) {
24814
+ const scripts = program.command("scripts").description("Manage script objects (the versioned JS source a trigger rule runs).");
24815
+ 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)");
24816
+ addCommonAuthOptions(push).action(
24817
+ async (name, flags) => {
24818
+ process.exit(
24819
+ await runScriptsPush(
24820
+ {
24821
+ ...toCommonOptions(flags),
24822
+ name,
24823
+ context: flags.context,
24824
+ sourceFile: flags.sourceFile,
24825
+ inputContract: flags.inputContract,
24826
+ inputContractFile: flags.inputContractFile,
24827
+ ifChanged: Boolean(flags.ifChanged),
24828
+ tenant: parseTenantFlag(flags.tenant)
24829
+ },
24830
+ deps
24831
+ )
24832
+ );
24833
+ }
24834
+ );
24835
+ 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)");
24836
+ addCommonAuthOptions(list).action(
24837
+ async (flags) => {
24838
+ process.exit(
24839
+ await runScriptsList(
24840
+ { ...toCommonOptions(flags), context: flags.context, name: flags.name, tenant: parseTenantFlag(flags.tenant) },
24841
+ deps
24842
+ )
24843
+ );
24844
+ }
24845
+ );
24846
+ 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)");
24847
+ addCommonAuthOptions(get).action(
24848
+ async (id, flags) => {
24849
+ process.exit(
24850
+ await runScriptsGet(
24851
+ { ...toCommonOptions(flags), id, context: flags.context, tenant: parseTenantFlag(flags.tenant) },
24852
+ deps
24853
+ )
24854
+ );
24855
+ }
24856
+ );
24857
+ 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)");
24858
+ addCommonAuthOptions(del).action(
24859
+ async (id, flags) => {
24860
+ process.exit(
24861
+ await runScriptsDelete(
24862
+ {
24863
+ ...toCommonOptions(flags),
24864
+ id,
24865
+ context: flags.context,
24866
+ tenant: parseTenantFlag(flags.tenant),
24867
+ force: Boolean(flags.force)
24868
+ },
24869
+ deps
24870
+ )
24871
+ );
24872
+ }
24873
+ );
24874
+ }
22393
24875
  function registerRoleCommands(program, deps) {
22394
24876
  const role = program.command("role").description("Manage context-scoped roles (reusable scope rules; bind via `access grant --role`).");
22395
24877
  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(
@@ -22398,72 +24880,106 @@ function registerRoleCommands(program, deps) {
22398
24880
  ).option(
22399
24881
  "--assumable <json>",
22400
24882
  `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)`
22401
- ).option("--description <text>", "free-text description");
24883
+ ).option(
24884
+ "--assignable-role <roleId>",
24885
+ '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"',
24886
+ collectOptional
24887
+ ).option("--description <text>", "free-text description").option("--tenant <tenant>", "tenant to target: 'test' or 'live' (default live)");
22402
24888
  addCommonAuthOptions(create).action(
22403
24889
  async (flags) => {
22404
- process.exit(
22405
- await runRoleCreate({
22406
- ...toCommonOptions(flags),
22407
- context: flags.context,
22408
- roleId: flags.roleId,
22409
- name: flags.name,
22410
- actions: flags.actions,
22411
- dataScope: flags.dataScope,
22412
- assumable: flags.assumable,
22413
- description: flags.description
22414
- }, deps)
22415
- );
24890
+ process.exitCode = await runRoleCreate({
24891
+ ...toCommonOptions(flags),
24892
+ tenant: parseTenantFlag(flags.tenant),
24893
+ context: flags.context,
24894
+ roleId: flags.roleId,
24895
+ name: flags.name,
24896
+ actions: flags.actions,
24897
+ dataScope: flags.dataScope,
24898
+ assumable: flags.assumable,
24899
+ assignableRole: flags.assignableRole,
24900
+ description: flags.description
24901
+ }, deps);
22416
24902
  }
22417
24903
  );
22418
- const list = role.command("list").description("List the roles defined in a context.").requiredOption("--context <c>", "app context");
24904
+ 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)");
22419
24905
  addCommonAuthOptions(list).action(async (flags) => {
22420
- process.exit(await runRoleList({ ...toCommonOptions(flags), context: flags.context }, deps));
22421
- });
22422
- const get = role.command("get").description("Show one role by id.").requiredOption("--context <c>", "app context").requiredOption("--role-id <id>", "role id");
22423
- addCommonAuthOptions(get).action(async (flags) => {
22424
- process.exit(await runRoleGet({ ...toCommonOptions(flags), context: flags.context, roleId: flags.roleId }, deps));
22425
- });
22426
- 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");
22427
- addAuthOptions(del).action(async (flags) => {
22428
- process.exit(await runRoleDelete({ ...toCommonOptions(flags), context: flags.context, roleId: flags.roleId }, deps));
24906
+ process.exitCode = await runRoleList(
24907
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context },
24908
+ deps
24909
+ );
22429
24910
  });
24911
+ 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)");
24912
+ addCommonAuthOptions(get).action(
24913
+ async (flags) => {
24914
+ process.exitCode = await runRoleGet(
24915
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, roleId: flags.roleId },
24916
+ deps
24917
+ );
24918
+ }
24919
+ );
24920
+ 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)");
24921
+ addAuthOptions(del).action(
24922
+ async (flags) => {
24923
+ process.exitCode = await runRoleDelete(
24924
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, roleId: flags.roleId },
24925
+ deps
24926
+ );
24927
+ }
24928
+ );
22430
24929
  }
22431
24930
  function registerAccessCommands(program, deps) {
22432
24931
  const access2 = program.command("access").description("Manage access bindings: principal \u2194 context \u2194 role/scopes.");
22433
- 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)");
24932
+ 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)");
22434
24933
  addCommonAuthOptions(grant).action(
22435
24934
  async (flags) => {
22436
- process.exit(
22437
- await runAccessGrant({
22438
- ...toCommonOptions(flags),
22439
- principal: flags.principal,
22440
- context: flags.context,
22441
- role: flags.role,
22442
- actions: flags.actions,
22443
- scope: flags.scope,
22444
- identityOverrides: flags.identityOverrides
22445
- }, deps)
24935
+ process.exitCode = await runAccessGrant({
24936
+ ...toCommonOptions(flags),
24937
+ tenant: parseTenantFlag(flags.tenant),
24938
+ principal: flags.principal,
24939
+ context: flags.context,
24940
+ role: flags.role,
24941
+ actions: flags.actions,
24942
+ scope: flags.scope,
24943
+ identityOverrides: flags.identityOverrides
24944
+ }, deps);
24945
+ }
24946
+ );
24947
+ 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)");
24948
+ addAuthOptions(revoke).action(
24949
+ async (flags) => {
24950
+ process.exitCode = await runAccessRevoke(
24951
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
24952
+ deps
24953
+ );
24954
+ }
24955
+ );
24956
+ 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)");
24957
+ addCommonAuthOptions(list).action(
24958
+ async (flags) => {
24959
+ process.exitCode = await runAccessList(
24960
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), context: flags.context, principal: flags.principal },
24961
+ deps
24962
+ );
24963
+ }
24964
+ );
24965
+ 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)");
24966
+ addCommonAuthOptions(get).action(
24967
+ async (flags) => {
24968
+ process.exitCode = await runAccessGet(
24969
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
24970
+ deps
24971
+ );
24972
+ }
24973
+ );
24974
+ 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)");
24975
+ addCommonAuthOptions(explain).action(
24976
+ async (flags) => {
24977
+ process.exitCode = await runAccessExplain(
24978
+ { ...toCommonOptions(flags), tenant: parseTenantFlag(flags.tenant), principal: flags.principal, context: flags.context },
24979
+ deps
22446
24980
  );
22447
24981
  }
22448
24982
  );
22449
- 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");
22450
- addAuthOptions(revoke).action(async (flags) => {
22451
- process.exit(await runAccessRevoke({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps));
22452
- });
22453
- 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)");
22454
- addCommonAuthOptions(list).action(async (flags) => {
22455
- process.exit(await runAccessList({ ...toCommonOptions(flags), context: flags.context, principal: flags.principal }, deps));
22456
- });
22457
- 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");
22458
- addCommonAuthOptions(get).action(async (flags) => {
22459
- process.exit(await runAccessGet({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps));
22460
- });
22461
- 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");
22462
- addCommonAuthOptions(explain).action(async (flags) => {
22463
- process.exit(
22464
- await runAccessExplain({ ...toCommonOptions(flags), principal: flags.principal, context: flags.context }, deps)
22465
- );
22466
- });
22467
24983
  }
22468
24984
  function dimensionOption() {
22469
24985
  return new import_commander.Option(