@vectros-ai/cli 0.11.0 → 0.13.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
@@ -1614,7 +1614,7 @@ var require_proper_lockfile = __commonJS({
1614
1614
  // src/cli-bootstrap.ts
1615
1615
  var readline = __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)
@@ -4043,7 +4043,7 @@ var AuthClient = class {
4043
4043
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/access-log");
4044
4044
  }
4045
4045
  /**
4046
- * Lists all of your scoped API keys (`ssk_*`) across both your live and test environments. Revoked keys are excluded. Requires the `keys:r` scope.
4046
+ * Lists your scoped API keys (`ssk_*`) in your credential's own environment — a live key lists live keys, a test key lists test keys. Revoked keys are excluded. Requires the `keys:r` scope.
4047
4047
  *
4048
4048
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
4049
4049
  *
@@ -4426,7 +4426,7 @@ var AuthClient = class {
4426
4426
  );
4427
4427
  }
4428
4428
  /**
4429
- * Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile 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 profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
4429
+ * Creates a new access profile under the given app context. This call is idempotent by `principalId`: if a profile with the same `principalId` already exists, the existing profile 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 profile was returned) tells the two apart. To overwrite an existing profile's `scopes`/`roleId`, `identityOverrides`, and `status` instead of returning it unchanged, set `?upsert=true` (this also requires the `profiles:u` scope, and applies the same `identityOverrides` bounds the update endpoint documents — a scoped credential may not repoint or clear an identity value it does not itself hold). You must provide exactly one of `scopes` (an inline list of scopes) or `roleId` (a reference to a role); supplying both, or neither, is rejected. `identityOverrides` is keyed by ownership namespace in `scope:<namespace>` form — `scope:org` and `scope:client` for the reserved namespaces, or any namespace you have registered — and may name at most two; any other key (including the account identifier or `userId`) is rejected. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:c` scope.
4430
4430
  *
4431
4431
  * @param {Vectros.CreateAccessProfileRequest} request
4432
4432
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4844,7 +4844,7 @@ var AuthClient = class {
4844
4844
  );
4845
4845
  }
4846
4846
  /**
4847
- * Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own; a root API key (`sk_`) is exempt. Requires the `profiles:u` scope.
4847
+ * Updates an access profile. This is a partial update: any field you omit (or send as null) keeps its existing value. A profile must reference either inline `scopes` or a `roleId`, never both — so setting `scopes` clears any `roleId`, and setting `roleId` clears any inline `scopes`. The `contextId` and `principalId` are immutable. Status changes (for example active to suspended) take effect within about five minutes. If you use a scoped credential, the profile's effective scopes may not exceed your own, and its `identityOverrides` are bounded twice: you may only set a value your own identity holds, and you may only change or clear a value the profile already holds if that value is yours as well. Repointing or clearing another principal's established identity therefore returns 403. A root API key (`sk_`) is exempt. Requires the `profiles:u` scope.
4848
4848
  *
4849
4849
  * @param {Vectros.UpdateAccessProfileRequest} request
4850
4850
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4917,7 +4917,7 @@ var AuthClient = class {
4917
4917
  );
4918
4918
  }
4919
4919
  /**
4920
- * Deletes an access profile. Within about five minutes (the access-profile cache lifetime), token minting for this principal in this context will be denied. Requires the `profiles:d` scope.
4920
+ * Deletes an access profile. Within about five minutes (the access-profile cache lifetime), token minting for this principal in this context will be denied. If you use a scoped credential and the profile carries `identityOverrides`, you may only delete it when you hold those values yourself — deleting a profile removes its identity, so the same bound applies as when clearing it. A profile with no `identityOverrides` is unaffected, and a root API key (`sk_`) is exempt. Requires the `profiles:d` scope.
4921
4921
  *
4922
4922
  * @param {Vectros.DeleteAccessProfileRequest} request
4923
4923
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5771,7 +5771,7 @@ var AuthClient = class {
5771
5771
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
5772
5772
  }
5773
5773
  /**
5774
- * Invite a new member to one of your app contexts by email. Creates a pending user with a pre-resolved access profile (their permissions on accept) and signs an invitation token. This call is idempotent on the combination of context and email: re-inviting the same email in the same context rotates the token and resends the invitation rather than creating a duplicate. Returns HTTP 201 on a new invite or a successful resend. Returns 409 if that email already belongs to an active or suspended member of the app context, or already has an identity elsewhere in your account (an email can currently belong to only one tenant per account, i.e. your test and live environments cannot share an email). When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `admin:users` scope.
5774
+ * Invite a new member to one of your app contexts by email. Creates a pending user with a pre-resolved access profile (their permissions on accept) and signs an invitation token. This call is idempotent on the combination of context and email: re-inviting the same email in the same context rotates the token and resends the invitation rather than creating a duplicate — this requires the `users:r` and `users:u` scopes in addition to `users:c`, because resending rotates a credential on an existing invitation and invalidates any link already sent. Without them the collision returns 409 instead, with no invitation details and no change to the outstanding invitation. Returns HTTP 201 on a new invite or a successful resend. Returns 409 if that email already belongs to an active or suspended member of the app context, or already has an identity elsewhere in your account (an email can currently belong to only one tenant per account, i.e. your test and live environments cannot share an email). When `sendEmail` is false, the response includes the raw token and a ready-to-use accept link so you can deliver the invitation through your own email provider. Requires the `users:c` scope.
5775
5775
  *
5776
5776
  * @param {Vectros.CreateInviteRequest} request
5777
5777
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5842,7 +5842,7 @@ var AuthClient = class {
5842
5842
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
5843
5843
  }
5844
5844
  /**
5845
- * Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Requires the `admin:users` scope.
5845
+ * Resend an outstanding invitation, identified by its email and app context. Rotates the invitation token and extends its expiry, then (when `sendEmail` is true) re-delivers the email. Rotating the token invalidates any previously issued link for this invitation, so only the newest link works. The invitee's pending permissions are left unchanged. Because this rotates a credential on an existing invitation, it requires the `users:c`, `users:r` and `users:u` scopes.
5846
5846
  *
5847
5847
  * @param {Vectros.CreateInviteRequest} request
5848
5848
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6176,7 +6176,7 @@ var DocumentsClient = class {
6176
6176
  * userId: "550e8400-e29b-41d4-a716-446655440000",
6177
6177
  * scope: "group:eng-team",
6178
6178
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
6179
- * startFrom: "doc_prev123"
6179
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
6180
6180
  * })
6181
6181
  */
6182
6182
  listDocuments(request = {}, requestOptions) {
@@ -6224,7 +6224,7 @@ var DocumentsClient = class {
6224
6224
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents");
6225
6225
  }
6226
6226
  /**
6227
- * Creates a document from a raw text string and queues it for asynchronous indexing so it becomes searchable. Optionally supply an `externalId` to make the create idempotent — if a document with the same `externalId` already exists in your context, that existing document is returned unchanged instead of a duplicate being created. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing document was returned) tells the two apart. To overwrite an existing document's content instead of returning it unchanged, set `?upsert=true` (this also requires the `documents:u` scope). Requires the `documents:c` scope.
6227
+ * Creates a document from a raw text string and queues it for asynchronous indexing so it becomes searchable. Optionally supply an `externalId` to make the create idempotent — if a document with the same `externalId` already exists in your context, that existing document is returned unchanged instead of a duplicate being created. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing document was returned) tells the two apart. To overwrite an existing document's content instead of returning it unchanged, set `?upsert=true` (this also requires the `documents:u` scope). Requires the `documents:c` scope to create. Being returned the existing document on a collision is a read of that document's data and additionally requires the `documents:r` scope — a credential holding `documents:c` alone receives a `400` ("already exists") on collision instead of the document.
6228
6228
  *
6229
6229
  * @param {Vectros.IngestDocumentRequest} request
6230
6230
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6486,6 +6486,7 @@ var DocumentsClient = class {
6486
6486
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
6487
6487
  *
6488
6488
  * @throws {@link Vectros.BadRequestError}
6489
+ * @throws {@link Vectros.ForbiddenError}
6489
6490
  * @throws {@link Vectros.NotFoundError}
6490
6491
  * @throws {@link Vectros.ConflictError}
6491
6492
  * @throws {@link Vectros.TooManyRequestsError}
@@ -6533,6 +6534,8 @@ var DocumentsClient = class {
6533
6534
  switch (_response.error.statusCode) {
6534
6535
  case 400:
6535
6536
  throw new BadRequestError(_response.error.body, _response.rawResponse);
6537
+ case 403:
6538
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
6536
6539
  case 404:
6537
6540
  throw new NotFoundError(_response.error.body, _response.rawResponse);
6538
6541
  case 409:
@@ -6550,25 +6553,29 @@ var DocumentsClient = class {
6550
6553
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PATCH", "/v1/documents/{id}");
6551
6554
  }
6552
6555
  /**
6553
- * Finds documents of a given type by field value. Supported fields: `externalId` (the document's first-class external identifier — no schema declaration required) and any field declared as a lookup field on the bound schema. A lookup on a sensitive field is rejected here because the value would appear in the URL query string; use POST /v1/documents/lookup (the request-body variant) for a sensitive field instead. Results are paginated: set `limit` for the page size and feed the returned `nextCursor` back as `startFrom` to fetch the next page. The response is a `{data, nextCursor}` envelope. Requires the `documents:r` scope.
6556
+ * Finds documents of a given type by field value. Supported fields: `externalId` (the document's first-class external identifier — no schema declaration required) and any field declared as a lookup field on the bound schema. A lookup on a sensitive field is rejected here because the value would appear in the URL query string; use POST /v1/documents/lookup (the request-body variant) for a sensitive field instead. `type`'s schema resolves with basedOn-aware shadowing: your own `userId`- or `scope`-owned variant if you have one, otherwise the shared base — for a scoped credential the owner is always your own token identity; `userId`/`scope` here only apply as an explicit owner selector for a root API key. Results are paginated: set `limit` for the page size and feed the returned `nextCursor` back as `startFrom` to fetch the next page. The response is a `{data, nextCursor}` envelope. Requires the `documents:r` scope.
6554
6557
  *
6555
6558
  * @param {Vectros.LookupDocumentsRequest} request
6556
6559
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
6557
6560
  *
6561
+ * @throws {@link Vectros.BadRequestError}
6562
+ *
6558
6563
  * @example
6559
6564
  * await client.documents.lookupDocuments({
6560
6565
  * type: "invoice",
6561
6566
  * field: "po_number",
6562
6567
  * value: "PO-1001",
6563
6568
  * prefix: "PO-2024",
6564
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
6569
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
6570
+ * userId: "550e8400-e29b-41d4-a716-446655440000",
6571
+ * scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
6565
6572
  * })
6566
6573
  */
6567
6574
  lookupDocuments(request, requestOptions) {
6568
6575
  return HttpResponsePromise.fromPromise(this.__lookupDocuments(request, requestOptions));
6569
6576
  }
6570
6577
  async __lookupDocuments(request, requestOptions) {
6571
- const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, order } = request;
6578
+ const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, order, userId, scope } = request;
6572
6579
  const _queryParams = {
6573
6580
  type: type_,
6574
6581
  field,
@@ -6578,7 +6585,9 @@ var DocumentsClient = class {
6578
6585
  prefix,
6579
6586
  startFrom,
6580
6587
  limit,
6581
- order: order != null ? order : void 0
6588
+ order: order != null ? order : void 0,
6589
+ userId,
6590
+ scope
6582
6591
  };
6583
6592
  const _authRequest = await this._options.authProvider.getAuthRequest();
6584
6593
  const _headers = mergeHeaders(
@@ -6604,11 +6613,16 @@ var DocumentsClient = class {
6604
6613
  return { data: _response.body, rawResponse: _response.rawResponse };
6605
6614
  }
6606
6615
  if (_response.error.reason === "status-code") {
6607
- throw new VectrosError({
6608
- statusCode: _response.error.statusCode,
6609
- body: _response.error.body,
6610
- rawResponse: _response.rawResponse
6611
- });
6616
+ switch (_response.error.statusCode) {
6617
+ case 400:
6618
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6619
+ default:
6620
+ throw new VectrosError({
6621
+ statusCode: _response.error.statusCode,
6622
+ body: _response.error.body,
6623
+ rawResponse: _response.rawResponse
6624
+ });
6625
+ }
6612
6626
  }
6613
6627
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/lookup");
6614
6628
  }
@@ -6928,7 +6942,7 @@ var FoldersClient = class {
6928
6942
  * parentFolderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
6929
6943
  * userId: "550e8400-e29b-41d4-a716-446655440000",
6930
6944
  * scope: "group:eng-team",
6931
- * startFrom: "fld_prev123"
6945
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
6932
6946
  * })
6933
6947
  */
6934
6948
  listFolders(request = {}, requestOptions) {
@@ -6976,7 +6990,7 @@ var FoldersClient = class {
6976
6990
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/folders");
6977
6991
  }
6978
6992
  /**
6979
- * Creates a folder to organize your documents and records. If `parentFolderId` is omitted, the folder is created under your context's default root folder. Folder creation is idempotent by (slug + parent): if a folder with the same slug already exists under the same parent, that existing folder is returned unchanged instead of a duplicate being created. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing folder was returned) tells the two apart. To overwrite an existing folder's mutable fields instead of returning it unchanged, set `?upsert=true` (this also requires the `folders:u` scope). Requires the `folders:c` scope.
6993
+ * Creates a folder to organize your documents and records. If `parentFolderId` is omitted, the folder is created under your context's default root folder. Folder creation is idempotent by (slug + parent): if a folder with the same slug already exists under the same parent, that existing folder is returned unchanged instead of a duplicate being created. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing folder was returned) tells the two apart. To overwrite an existing folder's mutable fields instead of returning it unchanged, set `?upsert=true` (this also requires the `folders:u` scope). Requires the `folders:c` scope to create. Being returned the existing folder on a collision is a read of that folder's data and additionally requires the `folders:r` scope — a credential holding `folders:c` alone receives a `400` ("already exists") on collision instead of the folder.
6980
6994
  *
6981
6995
  * @param {Vectros.CreateFolderRequest} request
6982
6996
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7105,6 +7119,7 @@ var FoldersClient = class {
7105
7119
  * @param {Vectros.UpdateFolderRequest} request
7106
7120
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
7107
7121
  *
7122
+ * @throws {@link Vectros.ForbiddenError}
7108
7123
  * @throws {@link Vectros.NotFoundError}
7109
7124
  * @throws {@link Vectros.TooManyRequestsError}
7110
7125
  *
@@ -7149,6 +7164,8 @@ var FoldersClient = class {
7149
7164
  }
7150
7165
  if (_response.error.reason === "status-code") {
7151
7166
  switch (_response.error.statusCode) {
7167
+ case 403:
7168
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
7152
7169
  case 404:
7153
7170
  throw new NotFoundError(_response.error.body, _response.rawResponse);
7154
7171
  case 429:
@@ -7231,6 +7248,7 @@ var FoldersClient = class {
7231
7248
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
7232
7249
  *
7233
7250
  * @throws {@link Vectros.BadRequestError}
7251
+ * @throws {@link Vectros.ForbiddenError}
7234
7252
  * @throws {@link Vectros.NotFoundError}
7235
7253
  * @throws {@link Vectros.ConflictError}
7236
7254
  * @throws {@link Vectros.TooManyRequestsError}
@@ -7278,6 +7296,8 @@ var FoldersClient = class {
7278
7296
  switch (_response.error.statusCode) {
7279
7297
  case 400:
7280
7298
  throw new BadRequestError(_response.error.body, _response.rawResponse);
7299
+ case 403:
7300
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
7281
7301
  case 404:
7282
7302
  throw new NotFoundError(_response.error.body, _response.rawResponse);
7283
7303
  case 409:
@@ -7365,7 +7385,8 @@ var IdentityClient = class {
7365
7385
  *
7366
7386
  * @example
7367
7387
  * await client.identity.listEntities({
7368
- * namespace: "team"
7388
+ * namespace: "team",
7389
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7369
7390
  * })
7370
7391
  */
7371
7392
  listEntities(request, requestOptions) {
@@ -7434,7 +7455,7 @@ var IdentityClient = class {
7434
7455
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/entities/{namespace}");
7435
7456
  }
7436
7457
  /**
7437
- * Creates a new entity in the given namespace. This call is idempotent on `externalId` within the namespace: if an entity with the same `externalId` already exists, the existing record is returned instead of creating a duplicate (`created: false`, HTTP 200). To overwrite an existing entity's content instead of returning it unchanged, set `?upsert=true` (also requires the `entities:u:<namespace>` scope). The namespace must be entity-backed (`org`/`client`, or registered via `POST /v1/namespaces`). Requires the `entities:c:<namespace>` scope.
7458
+ * Creates a new entity in the given namespace. This call is idempotent on `externalId` within the namespace: if an entity with the same `externalId` already exists, the existing record is returned instead of creating a duplicate (`created: false`, HTTP 200). To overwrite an existing entity's content instead of returning it unchanged, set `?upsert=true` (also requires the `entities:u:<namespace>` scope). The namespace must be entity-backed (`org`/`client`, or registered via `POST /v1/namespaces`). Requires the `entities:c:<namespace>` scope to create. Being returned the existing entity on a collision is a read of that entity's data and additionally requires the `entities:r:<namespace>` scope — a credential holding `entities:c:<namespace>` alone receives a `400` ("already in use") on collision instead of the entity.
7438
7459
  *
7439
7460
  * @param {Vectros.CreateEntityRequest} request
7440
7461
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7878,7 +7899,7 @@ var IdentityClient = class {
7878
7899
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
7879
7900
  }
7880
7901
  /**
7881
- * Updates the mutable fields (`entityBacked`, `defaultSchemaId`) of a registered namespace. The namespace name itself is immutable. Requires a root API key. The reserved built-ins cannot be updated.
7902
+ * Updates the mutable fields (`entityBacked`, `defaultSchemaId`, `specificityRank`) of a registered namespace. The namespace name itself is immutable. Requires a root API key. The reserved built-ins cannot be updated.
7882
7903
  *
7883
7904
  * @param {Vectros.UpdateNamespaceRequest} request
7884
7905
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7891,7 +7912,8 @@ var IdentityClient = class {
7891
7912
  * await client.identity.updateNamespace({
7892
7913
  * namespace: "team",
7893
7914
  * body: {
7894
- * namespace: "team"
7915
+ * namespace: "team",
7916
+ * specificityRank: 1500
7895
7917
  * }
7896
7918
  * })
7897
7919
  */
@@ -8056,7 +8078,7 @@ var IdentityClient = class {
8056
8078
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
8057
8079
  }
8058
8080
  /**
8059
- * Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Requires a root API key. The reserved names `org` and `client` are built in and cannot be registered.
8081
+ * Registers a new scope namespace and declares whether its values resolve to identity entities (`entityBacked`). Also requires `specificityRank`, an explicit, account-unique position in the specificity order used to break recordType schema-resolution ties. Requires a root API key. The reserved names `org` and `client` are built in and cannot be registered.
8060
8082
  *
8061
8083
  * @param {Vectros.NamespaceRequest} request
8062
8084
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8067,7 +8089,8 @@ var IdentityClient = class {
8067
8089
  *
8068
8090
  * @example
8069
8091
  * await client.identity.registerNamespace({
8070
- * namespace: "team"
8092
+ * namespace: "team",
8093
+ * specificityRank: 1500
8071
8094
  * })
8072
8095
  */
8073
8096
  registerNamespace(request, requestOptions) {
@@ -8127,7 +8150,7 @@ var IdentityClient = class {
8127
8150
  * @example
8128
8151
  * await client.identity.listUsers({
8129
8152
  * externalId: "usr_12345",
8130
- * startFrom: "550e8400-e29b-41d4-a716-446655440000",
8153
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
8131
8154
  * type: "person_v1",
8132
8155
  * field: "team",
8133
8156
  * value: "engineering",
@@ -8184,7 +8207,7 @@ var IdentityClient = class {
8184
8207
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users");
8185
8208
  }
8186
8209
  /**
8187
- * Creates a user identity in your account. The operation is idempotent on `externalId`: if a user with the same `externalId` already exists, the existing record is returned instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing user was returned) tells the two apart. To overwrite an existing user's mutable fields (email, status, payload, schema binding) instead of returning it unchanged, set `?upsert=true` (this also requires the `users:u` scope). Requires the `users:c` scope.
8210
+ * Creates a user identity in your account. The operation is idempotent on `externalId`: if a user with the same `externalId` already exists, the existing record is returned instead of creating a duplicate. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing user was returned) tells the two apart. To overwrite an existing user's mutable fields (email, status, payload, schema binding) instead of returning it unchanged, set `?upsert=true` (this also requires the `users:u` scope). Requires the `users:c` scope to create. Being returned the existing user on a collision is a read of that user's data and additionally requires the `users:r` scope — a credential holding `users:c` alone receives a `400` ("already exists") on collision instead of the user.
8188
8211
  *
8189
8212
  * @param {Vectros.CreateUserRequest} request
8190
8213
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8308,11 +8331,12 @@ var IdentityClient = class {
8308
8331
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
8309
8332
  }
8310
8333
  /**
8311
- * Updates mutable fields on an existing user (such as email, status, payload, or schema binding). The `type` field is immutable after creation. This endpoint also activates an invited user: a PUT that moves a PENDING user to ACTIVE and carries `inviteToken`, `externalSubject`, and `emailVerifiedAttestation=true` completes the invitation. Requires the `users:u` scope.
8334
+ * Updates mutable fields on an existing user (such as email, status, payload, or schema binding). The `type` field is immutable after creation, and `email` cannot be changed while an invitation to that user is still outstanding — revoke the invitation, or invite the new address instead. This endpoint also activates an invited user: a PUT that moves a PENDING user to ACTIVE and carries `inviteToken`, `externalSubject`, and `emailVerifiedAttestation=true` completes the invitation. Requires the `users:u` scope.
8312
8335
  *
8313
8336
  * @param {Vectros.UpdateUserRequest} request
8314
8337
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8315
8338
  *
8339
+ * @throws {@link Vectros.BadRequestError}
8316
8340
  * @throws {@link Vectros.NotFoundError}
8317
8341
  * @throws {@link Vectros.TooManyRequestsError}
8318
8342
  *
@@ -8357,6 +8381,8 @@ var IdentityClient = class {
8357
8381
  }
8358
8382
  if (_response.error.reason === "status-code") {
8359
8383
  switch (_response.error.statusCode) {
8384
+ case 400:
8385
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
8360
8386
  case 404:
8361
8387
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8362
8388
  case 429:
@@ -9004,7 +9030,7 @@ var RecordsClient = class {
9004
9030
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
9005
9031
  * userId: "550e8400-e29b-41d4-a716-446655440000",
9006
9032
  * scope: "group:eng-team",
9007
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
9033
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9008
9034
  * })
9009
9035
  */
9010
9036
  listRecords(request = {}, requestOptions) {
@@ -9055,7 +9081,7 @@ var RecordsClient = class {
9055
9081
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/records");
9056
9082
  }
9057
9083
  /**
9058
- * Creates a new record of a given type. The `payload` is validated against that type's schema before the record is stored. Identify the type by sending `typeName`, `schemaId`, or both (they must agree); if you send only `schemaId`, the type is taken from that schema. Optionally supply an `externalId` to make the create idempotent — if a record with the same `externalId` already exists in your context, that existing record is returned unchanged instead of a duplicate being created. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing record was returned) tells the two apart. To overwrite an existing record's content instead of returning it unchanged, set `?upsert=true` (this also requires the `records:u:<type>` scope). Requires the `records:c:<type>` scope.
9084
+ * Creates a new record of a given type. The `payload` is validated against that type's schema before the record is stored. Identify the type by sending `typeName`, `schemaId`, or both (they must agree); if you send only `schemaId`, the type is taken from that schema. Optionally supply an `externalId` to make the create idempotent — if a record with the same `externalId` already exists in your context, that existing record is returned unchanged instead of a duplicate being created. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing record was returned) tells the two apart. To overwrite an existing record's content instead of returning it unchanged, set `?upsert=true` (this also requires the `records:u:<type>` scope). Requires the `records:c:<type>` scope to create. Being returned the existing record on a collision is a read of that record's data and additionally requires the `records:r:<type>` scope — a credential holding `records:c:<type>` alone receives a `400` ("already exists") on collision instead of the record.
9059
9085
  *
9060
9086
  * @param {Vectros.CreateRecordRequest} request
9061
9087
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9192,6 +9218,7 @@ var RecordsClient = class {
9192
9218
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9193
9219
  *
9194
9220
  * @throws {@link Vectros.BadRequestError}
9221
+ * @throws {@link Vectros.ForbiddenError}
9195
9222
  * @throws {@link Vectros.NotFoundError}
9196
9223
  * @throws {@link Vectros.ConflictError}
9197
9224
  * @throws {@link Vectros.TooManyRequestsError}
@@ -9240,6 +9267,8 @@ var RecordsClient = class {
9240
9267
  switch (_response.error.statusCode) {
9241
9268
  case 400:
9242
9269
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9270
+ case 403:
9271
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9243
9272
  case 404:
9244
9273
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9245
9274
  case 409:
@@ -9321,6 +9350,7 @@ var RecordsClient = class {
9321
9350
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9322
9351
  *
9323
9352
  * @throws {@link Vectros.BadRequestError}
9353
+ * @throws {@link Vectros.ForbiddenError}
9324
9354
  * @throws {@link Vectros.NotFoundError}
9325
9355
  * @throws {@link Vectros.ConflictError}
9326
9356
  * @throws {@link Vectros.TooManyRequestsError}
@@ -9371,6 +9401,8 @@ var RecordsClient = class {
9371
9401
  switch (_response.error.statusCode) {
9372
9402
  case 400:
9373
9403
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9404
+ case 403:
9405
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9374
9406
  case 404:
9375
9407
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9376
9408
  case 409:
@@ -9388,7 +9420,15 @@ var RecordsClient = class {
9388
9420
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PATCH", "/v1/records/{id}");
9389
9421
  }
9390
9422
  /**
9391
- * Finds records by the value of a lookup field declared on the type's schema. Provide exactly one lookup mode: `value` (exact match), `from`+`to` (inclusive range, ascending by value), or `prefix` (string fields only, ascending). Range and prefix lookups are not supported on a sensitive field, because its value is stored as a blind index and has no sortable order. An exact-`value` lookup on a sensitive field is also rejected on this GET endpoint — the value must not appear in the URL — so use the `POST /v1/records/lookup` body variant for sensitive fields. Results are paginated: set `limit` for the page size and pass the returned `nextCursor` back as `startFrom` for the next page. Requires the `records:r:<type>` scope.
9423
+ * Finds records by the value of a lookup field declared on the type's schema. Provide exactly one lookup mode: `value` (exact match), `from`+`to` (inclusive range, ascending by value), or `prefix` (string fields only, ascending). Range and prefix lookups are not supported on a sensitive field, because its value is stored as a blind index and has no sortable order. An exact-`value` lookup on a sensitive field is also rejected on this GET endpoint — the value must not appear in the URL — so use the `POST /v1/records/lookup` body variant for sensitive fields.
9424
+ *
9425
+ * A `value` lookup can additionally be narrowed to a window of the lookup field's **sort key** using `sortFrom` and/or `sortTo` (inclusive) — for example, one session's records created since a timestamp. The sort key is whatever the schema declares as that lookup's `sortBy` (`createdAt` by default, `lastUpdated`, or another field), and bounds are given in that field's own units — epoch milliseconds for the two timestamp options. **Records that have no value for the sorted field are never included in a bounded window** — they are ordered ahead of every record that does have one, and a `sortFrom`/`sortTo` window only ever selects from records carrying a value. The sorted field does not have to be `required`.
9426
+ *
9427
+ * Narrowing is available on any lookup field your schema declares for fast equality lookup, and on `externalId` — which is always ordered by creation time, so its bounds are epoch milliseconds whatever the schema says. It is rejected (`400`) for a field declared with `rangeEnabled`, for a field declared beyond the schema's fast-lookup budget, for a lookup whose `sortBy` names a sensitive field (a sensitive value is stored as a blind index and has no order), and for a window whose start is after its end. Ownership fields are not lookup fields on this endpoint at all — see the `field` parameter.
9428
+ *
9429
+ * While paging a narrowed lookup, keep every other parameter identical. The cursor is valid only for the exact query that returned it — changing `order`, `sortFrom`, `sortTo`, or dropping them altogether, is rejected rather than silently resumed at a position that means something different in the new query.
9430
+ *
9431
+ * Results are paginated: set `limit` for the page size and pass the returned `nextCursor` back as `startFrom` for the next page. **Keep paging until `nextCursor` is null** — a page can come back empty or shorter than `limit` while more results remain, so an empty page is not the end of the results. Requires the `records:r:<type>` scope.
9392
9432
  *
9393
9433
  * @param {Vectros.LookupRecordsRequest} request
9394
9434
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9400,22 +9440,40 @@ var RecordsClient = class {
9400
9440
  * type: "intake_form",
9401
9441
  * field: "email",
9402
9442
  * value: "jane@example.com",
9443
+ * values: ["open"],
9403
9444
  * prefix: "jane",
9404
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
9445
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9405
9446
  * })
9406
9447
  */
9407
9448
  lookupRecords(request, requestOptions) {
9408
9449
  return HttpResponsePromise.fromPromise(this.__lookupRecords(request, requestOptions));
9409
9450
  }
9410
9451
  async __lookupRecords(request, requestOptions) {
9411
- const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, includePayload, order } = request;
9452
+ const {
9453
+ type: type_,
9454
+ field,
9455
+ value,
9456
+ values,
9457
+ from: from_,
9458
+ to,
9459
+ prefix,
9460
+ sortFrom,
9461
+ sortTo,
9462
+ startFrom,
9463
+ limit,
9464
+ includePayload,
9465
+ order
9466
+ } = request;
9412
9467
  const _queryParams = {
9413
9468
  type: type_,
9414
9469
  field,
9415
9470
  value,
9471
+ values,
9416
9472
  from: from_,
9417
9473
  to,
9418
9474
  prefix,
9475
+ sortFrom,
9476
+ sortTo,
9419
9477
  startFrom,
9420
9478
  limit,
9421
9479
  includePayload,
@@ -9459,7 +9517,7 @@ var RecordsClient = class {
9459
9517
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/records/lookup");
9460
9518
  }
9461
9519
  /**
9462
- * Body-based equivalent of `GET /v1/records/lookup`. Use this when looking up by a sensitive field: the value travels in the request body (and is blind-indexed server-side) instead of in the URL query string, so it never lands in access, CDN, or proxy logs. The GET variant rejects an exact-value lookup on a sensitive field and directs you here. Non-sensitive exact-value, range (`from`+`to`), and prefix lookups also work here. Returns the same `{data, nextCursor}` envelope and uses the same pagination as the GET variant. Requires the `records:r:<type>` scope.
9520
+ * Body-based equivalent of `GET /v1/records/lookup`. Use this when looking up by a sensitive field: the value travels in the request body (and is blind-indexed server-side) instead of in the URL query string, so it never lands in access, CDN, or proxy logs. The GET variant rejects an exact-value lookup on a sensitive field and directs you here. Non-sensitive exact-value, range (`from`+`to`), and prefix lookups also work here, as does narrowing a `value` lookup by the field's sort key with `sortFrom`/`sortTo` — see the GET variant for what the sort key is and when narrowing by it is available. Returns the same `{data, nextCursor}` envelope and uses the same pagination as the GET variant; keep paging until `nextCursor` is null rather than stopping on an empty page. Requires the `records:r:<type>` scope.
9463
9521
  *
9464
9522
  * @param {Vectros.RecordLookupRequest} request
9465
9523
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9646,13 +9704,15 @@ var SchemasClient = class {
9646
9704
  * @param {Vectros.ListSchemasRequest} request
9647
9705
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
9648
9706
  *
9707
+ * @throws {@link Vectros.BadRequestError}
9708
+ *
9649
9709
  * @example
9650
9710
  * await client.schemas.listSchemas({
9651
9711
  * userId: "550e8400-e29b-41d4-a716-446655440000",
9652
9712
  * scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8",
9653
9713
  * surface: "document",
9654
9714
  * recordType: "intake_form",
9655
- * startFrom: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
9715
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9656
9716
  * })
9657
9717
  */
9658
9718
  listSchemas(request = {}, requestOptions) {
@@ -9692,16 +9752,21 @@ var SchemasClient = class {
9692
9752
  return { data: _response.body, rawResponse: _response.rawResponse };
9693
9753
  }
9694
9754
  if (_response.error.reason === "status-code") {
9695
- throw new VectrosError({
9696
- statusCode: _response.error.statusCode,
9697
- body: _response.error.body,
9698
- rawResponse: _response.rawResponse
9699
- });
9755
+ switch (_response.error.statusCode) {
9756
+ case 400:
9757
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
9758
+ default:
9759
+ throw new VectrosError({
9760
+ statusCode: _response.error.statusCode,
9761
+ body: _response.error.body,
9762
+ rawResponse: _response.rawResponse
9763
+ });
9764
+ }
9700
9765
  }
9701
9766
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/schemas");
9702
9767
  }
9703
9768
  /**
9704
- * Defines a new record type with optional field definitions, validation rules, and lookup indexes. Idempotent by `typeName` within the same ownership scope: re-creating an existing `typeName` returns the existing schema rather than failing. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing schema was returned) tells the two apart. To reconcile an existing schema to the submitted shape instead of returning it unchanged, set `?upsert=true` (this also requires the `schemas:w` scope; only legal schema changes are applied — migration-locked changes are rejected). Requires the `schemas:w` scope.
9769
+ * Defines a new record type with optional field definitions, validation rules, and lookup indexes. Idempotent by `typeName` within the same ownership scope: re-creating an existing `typeName` returns the existing schema rather than failing. The response's `created` field (and the HTTP status — 201 when created, 200 when an existing schema was returned) tells the two apart. To reconcile an existing schema to the submitted shape instead of returning it unchanged, set `?upsert=true` (this also requires the `schemas:u` scope; only legal schema changes are applied — migration-locked changes are rejected). Requires the `schemas:c` scope to create. Being returned the existing schema on a collision is a read of that schema's data and additionally requires the `schemas:r` scope — a credential holding `schemas:c` alone receives a `400` ("already in use") on collision instead of the schema.
9705
9770
  *
9706
9771
  * @param {Vectros.CreateSchemaRequest} request
9707
9772
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9845,12 +9910,13 @@ var SchemasClient = class {
9845
9910
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/schemas/{id}");
9846
9911
  }
9847
9912
  /**
9848
- * 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:w` scope.
9913
+ * 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.
9849
9914
  *
9850
9915
  * @param {Vectros.UpdateSchemaRequest} request
9851
9916
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
9852
9917
  *
9853
9918
  * @throws {@link Vectros.BadRequestError}
9919
+ * @throws {@link Vectros.ForbiddenError}
9854
9920
  * @throws {@link Vectros.NotFoundError}
9855
9921
  * @throws {@link Vectros.TooManyRequestsError}
9856
9922
  *
@@ -9899,6 +9965,8 @@ var SchemasClient = class {
9899
9965
  switch (_response.error.statusCode) {
9900
9966
  case 400:
9901
9967
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9968
+ case 403:
9969
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9902
9970
  case 404:
9903
9971
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9904
9972
  case 429:
@@ -9914,7 +9982,7 @@ var SchemasClient = class {
9914
9982
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/schemas/{id}");
9915
9983
  }
9916
9984
  /**
9917
- * 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. Requires the `schemas:w` scope.
9985
+ * 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.
9918
9986
  *
9919
9987
  * @param {Vectros.DeleteSchemaRequest} request
9920
9988
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10165,7 +10233,7 @@ var logging;
10165
10233
  logging2.ConsoleLogger = ConsoleLogger;
10166
10234
  })(logging || (logging = {}));
10167
10235
 
10168
- // ../blueprints/dist/index.mjs
10236
+ // node_modules/@vectros-ai/blueprints/dist/index.mjs
10169
10237
  var import_zod = require("zod");
10170
10238
  var import_zod2 = require("zod");
10171
10239
  var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
@@ -10269,19 +10337,63 @@ var BlueprintFieldDefSchema = import_zod.z.object({
10269
10337
  var BlueprintLookupFieldSchema = import_zod.z.union([
10270
10338
  import_zod.z.string().min(1),
10271
10339
  import_zod.z.object({
10272
- fieldName: import_zod.z.string().min(1),
10340
+ // A lookup field declares either ONE field (fieldName) or 2-3 fields
10341
+ // matched together (fieldNames) — never both, never neither. `.strict()`
10342
+ // below only rejects unknown KEYS; it does not enforce this combination,
10343
+ // so the `.superRefine()` after it carries the actual rule.
10344
+ fieldName: import_zod.z.string().min(1).optional(),
10345
+ // Composite (conjunctive) lookup: match on all of these fields at once —
10346
+ // "every record where `status` is `open` AND `area` is `billing`", exact,
10347
+ // in a stable order. Order is significant and locked at create: a query
10348
+ // may match a leading run of the list (the first field alone, the first
10349
+ // two together, …) but never a later field alone — declare a separate
10350
+ // lookup for that. Record-only: the schema's `allowedSurfaces` must be
10351
+ // exactly `['record']` (checked at the schema level below) — the
10352
+ // platform's composite index has no document/user/entity reader yet.
10353
+ fieldNames: import_zod.z.array(import_zod.z.string().min(1)).min(2).max(3).optional(),
10354
+ // Not available on a composite (`fieldNames`) lookup — checked below.
10273
10355
  unique: import_zod.z.boolean().optional(),
10274
10356
  // Opt this field into ordered range + prefix lookups (from/to/prefix) on
10275
10357
  // top of exact match. Billed at the range-index rate; not valid on a
10276
- // sensitive field (a blind index is not orderable). Locked at create.
10358
+ // sensitive field (a blind index is not orderable), and — like `unique`
10359
+ // — not available on a composite lookup: it is an exact-match index over
10360
+ // its fields; declare the range lookup separately. Locked at create.
10277
10361
  rangeEnabled: import_zod.z.boolean().optional(),
10278
10362
  // Sort key for the exact-match index: 'createdAt' (default), 'lastUpdated',
10279
- // or a declared field on this schema. Locked at create.
10363
+ // or a declared field on this schema. Locked at create. Valid on a
10364
+ // composite lookup too — it orders WITHIN a group when a query supplies
10365
+ // fewer values than the lookup declares, not across the whole result.
10280
10366
  sortBy: import_zod.z.string().min(1).optional(),
10281
10367
  // Opt a field past the fixed fast-index budget into a higher-cost
10282
10368
  // secondary index. No effect on a field that fits within the budget.
10283
10369
  allowOverflow: import_zod.z.boolean().optional()
10284
- }).strict()
10370
+ }).strict().superRefine((lf, ctx) => {
10371
+ const hasFieldName = lf.fieldName !== void 0;
10372
+ const hasFieldNames = lf.fieldNames !== void 0;
10373
+ if (hasFieldName === hasFieldNames) {
10374
+ ctx.addIssue({
10375
+ code: import_zod.z.ZodIssueCode.custom,
10376
+ message: "a lookup field declares either 'fieldName' (one field) or 'fieldNames' (2-3 fields matched together), never both and never neither"
10377
+ });
10378
+ return;
10379
+ }
10380
+ if (hasFieldNames) {
10381
+ if (lf.unique !== void 0) {
10382
+ ctx.addIssue({
10383
+ code: import_zod.z.ZodIssueCode.custom,
10384
+ path: ["unique"],
10385
+ message: "'unique' is not available on a composite ('fieldNames') lookup"
10386
+ });
10387
+ }
10388
+ if (lf.rangeEnabled !== void 0) {
10389
+ ctx.addIssue({
10390
+ code: import_zod.z.ZodIssueCode.custom,
10391
+ path: ["rangeEnabled"],
10392
+ message: "'rangeEnabled' is not available on a composite ('fieldNames') lookup"
10393
+ });
10394
+ }
10395
+ }
10396
+ })
10285
10397
  ]);
10286
10398
  var BlueprintCapabilitiesSchema = import_zod.z.object({
10287
10399
  auditHistory: import_zod.z.boolean().optional()
@@ -10311,8 +10423,29 @@ var BlueprintSchemaSchema = import_zod.z.object({
10311
10423
  // With a scoped token these must be consistent with the profile's dataScope
10312
10424
  // (a cross-consistency lint is deferred to the lint slice).
10313
10425
  userId: import_zod.z.string().min(1).optional(),
10314
- scopes: import_zod.z.array(import_zod.z.string().min(1)).max(2).optional()
10315
- }).strict();
10426
+ scopes: import_zod.z.array(import_zod.z.string().min(1)).max(2).optional(),
10427
+ // The id of an existing schema this one is a CUSTOMIZATION of, when a schema
10428
+ // named `typeName` already exists in this context — required in that case,
10429
+ // and must be omitted when this create is the first schema under that name
10430
+ // (it becomes that name's shared base). Must point directly at the base (one
10431
+ // hop); immutable once set. Mirrors the `basedOn` field on the platform's
10432
+ // schema-create request.
10433
+ basedOn: import_zod.z.string().min(1).optional()
10434
+ }).strict().superRefine((schema, ctx) => {
10435
+ const hasComposite = (schema.lookupFields ?? []).some(
10436
+ (lf) => typeof lf === "object" && lf.fieldNames !== void 0
10437
+ );
10438
+ if (!hasComposite) return;
10439
+ const surfaces = schema.allowedSurfaces;
10440
+ const isRecordOnly = surfaces === void 0 || surfaces.length === 1 && surfaces[0] === "record";
10441
+ if (!isRecordOnly) {
10442
+ ctx.addIssue({
10443
+ code: import_zod.z.ZodIssueCode.custom,
10444
+ path: ["allowedSurfaces"],
10445
+ message: `schema '${schema.typeName}': a composite ('fieldNames') lookup requires allowedSurfaces to be exactly ['record'] \u2014 the platform's composite index has no document/user/entity reader yet`
10446
+ });
10447
+ }
10448
+ });
10316
10449
  var BlueprintAccessProfileSchema = import_zod.z.object({
10317
10450
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10318
10451
  // what enforces the data-plane-only security boundary.
@@ -12251,9 +12384,10 @@ var secondBrain = {
12251
12384
  // Lookup shapes are MIGRATION-LOCKED once the schema is live (a field cannot
12252
12385
  // flip equality↔range later), so each is chosen on purpose:
12253
12386
  // • `source` — EQUALITY, sorted by `lastUpdated`: enumerate notes from one
12254
- // source, most-recently-touched first. `lastUpdated` is always present, so
12255
- // the sort never drops a note (sorting an equality lookup by an OPTIONAL
12256
- // field would silently exclude rows that lack it).
12387
+ // source, most-recently-touched first. A platform timestamp is the natural
12388
+ // choice for "most recently touched" and is always present, so every note
12389
+ // has a position. (Sorting by an OPTIONAL payload field is also fine — notes
12390
+ // lacking it are ordered ahead of those that have it, never dropped.)
12257
12391
  // • `status` — EQUALITY: "show archived notes".
12258
12392
  // • `capturedAt` — RANGE: ordered `from`/`to`/`prefix` ("captured in 2026-06").
12259
12393
  // `externalId` has a built-in first-class finder — look it up directly, never
@@ -13375,7 +13509,16 @@ function toSdkFields(blueprint, schemaIndex) {
13375
13509
  function toLookupDefs(lookupFields) {
13376
13510
  return lookupFields?.map((lf) => {
13377
13511
  if (typeof lf === "string") return { fieldName: lf };
13378
- const def = { fieldName: lf.fieldName };
13512
+ const hasFieldName = lf.fieldName !== void 0;
13513
+ const hasFieldNames = lf.fieldNames !== void 0;
13514
+ if (hasFieldName === hasFieldNames) {
13515
+ throw new Error(
13516
+ `Internal error: lookupField resolved to ${hasFieldName ? "both fieldName and fieldNames" : "neither fieldName nor fieldNames"} \u2014 @vectros-ai/blueprints' parseBlueprint should have refused this before the loader ever saw it.`
13517
+ );
13518
+ }
13519
+ const def = {};
13520
+ if (lf.fieldName !== void 0) def.fieldName = lf.fieldName;
13521
+ if (lf.fieldNames !== void 0) def.fieldNames = lf.fieldNames;
13379
13522
  if (lf.unique !== void 0) def.unique = lf.unique;
13380
13523
  if (lf.rangeEnabled !== void 0) def.rangeEnabled = lf.rangeEnabled;
13381
13524
  if (lf.sortBy !== void 0) def.sortBy = lf.sortBy;
@@ -13469,7 +13612,8 @@ async function runLoader(client, input) {
13469
13612
  capabilities: toCapabilities(s.capabilities),
13470
13613
  active: s.active,
13471
13614
  userId: s.userId,
13472
- scopes: s.scopes
13615
+ scopes: s.scopes,
13616
+ basedOn: s.basedOn
13473
13617
  };
13474
13618
  const resp = await ctxClient.schemas.createSchema({ upsert: true, body: schemaBody });
13475
13619
  if (resp.id) schemaIds[s.typeName] = resp.id;
@@ -13637,7 +13781,7 @@ function buildPlan(blueprint, opts = {}) {
13637
13781
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
13638
13782
  });
13639
13783
  for (const s of blueprint.schemas) {
13640
- const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldName) ?? [];
13784
+ const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
13641
13785
  const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
13642
13786
  const surfaceNote = s.allowedSurfaces ? ` surfaces: [${s.allowedSurfaces.join(", ")}]` : "";
13643
13787
  const sensitive = s.fields.filter((f) => f.sensitive).map((f) => f.fieldId);
@@ -14229,10 +14373,121 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
14229
14373
  // src/cli-blueprint.ts
14230
14374
  var import_promises3 = require("fs/promises");
14231
14375
 
14376
+ // src/scope-grammar.ts
14377
+ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
14378
+ "user",
14379
+ "record",
14380
+ "document",
14381
+ "entity",
14382
+ "self",
14383
+ "tenant",
14384
+ "context",
14385
+ "scope",
14386
+ "versions",
14387
+ "lookup"
14388
+ ]);
14389
+ var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
14390
+ var SCOPE_NAMESPACE_MIN_LENGTH = 2;
14391
+ var SCOPE_NAMESPACE_MAX_LENGTH = 32;
14392
+ var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
14393
+ var SCOPE_VALUE_MIN_LENGTH = 1;
14394
+ var SCOPE_VALUE_MAX_LENGTH = 128;
14395
+ function valueGrammarError(value) {
14396
+ if (!SCOPE_VALUE_RE.test(value)) {
14397
+ 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.`;
14398
+ }
14399
+ return null;
14400
+ }
14401
+ function namespaceGrammarError(ns) {
14402
+ if (FORBIDDEN_SCOPE_NAMESPACES.has(ns)) {
14403
+ return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
14404
+ }
14405
+ if (!SCOPE_NAMESPACE_RE.test(ns)) {
14406
+ 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 '-'`;
14407
+ }
14408
+ return null;
14409
+ }
14410
+ function assertScopeEntries(entries, flag) {
14411
+ if (!entries?.length) return;
14412
+ for (const raw of entries) {
14413
+ const sep = raw.indexOf(":");
14414
+ if (sep <= 0 || sep === raw.length - 1) {
14415
+ throw new Error(`${flag} must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
14416
+ }
14417
+ const nsError = namespaceGrammarError(raw.slice(0, sep));
14418
+ if (nsError) throw new Error(`${flag} namespace ${nsError}`);
14419
+ const valError = valueGrammarError(raw.slice(sep + 1));
14420
+ if (valError) throw new Error(`${flag} value ${valError}`);
14421
+ }
14422
+ }
14423
+ function assertIdentityOverrideValues(overrides, flag) {
14424
+ if (!overrides) return;
14425
+ for (const [key, value] of Object.entries(overrides)) {
14426
+ if (!key.startsWith("scope:")) continue;
14427
+ const error = valueGrammarError(typeof value === "string" ? value : String(value));
14428
+ if (error) throw new Error(`${flag} '${key}' value ${error}`);
14429
+ }
14430
+ }
14431
+ function placeholderScopeNamespace(value) {
14432
+ const m = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
14433
+ return m ? m[1] : null;
14434
+ }
14435
+ var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
14436
+ var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
14437
+
14232
14438
  // src/lint.ts
14233
14439
  var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
14234
- function lookupFieldName(lf) {
14235
- return typeof lf === "string" ? lf : lf.fieldName;
14440
+ var BUILT_IN_SORT_BY = /* @__PURE__ */ new Set(["createdAt", "lastUpdated"]);
14441
+ function lookupFieldNames(lf) {
14442
+ if (typeof lf === "string") return [lf];
14443
+ if (lf.fieldNames !== void 0) return lf.fieldNames;
14444
+ return lf.fieldName !== void 0 ? [lf.fieldName] : [];
14445
+ }
14446
+ function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
14447
+ const findings = [];
14448
+ const id = `[${lf.fieldNames.join(", ")}]`;
14449
+ const seenLegs = /* @__PURE__ */ new Set();
14450
+ for (const leg of lf.fieldNames) {
14451
+ if (seenLegs.has(leg)) {
14452
+ findings.push({
14453
+ severity: "error",
14454
+ rule: "lookup-composite-repeated-leg",
14455
+ message: `schema '${schema.typeName}': composite lookupField ${id} repeats the leg '${leg}'. Each leg occupies its own position in the sort key, so a repeat doubles the key cost of every row to express a conjunction of a field with itself.`
14456
+ });
14457
+ }
14458
+ seenLegs.add(leg);
14459
+ const field = fieldsById.get(leg);
14460
+ if (!field) continue;
14461
+ if (field.fieldType === "array" || field.fieldType === "object") {
14462
+ findings.push({
14463
+ severity: "error",
14464
+ rule: "lookup-composite-leg-multi-valued",
14465
+ message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but it is declared '${field.fieldType}'. A leg occupies one sort-key position and holds one value \u2014 a multi-valued or structured field would index only one rendering and the conjunction would return a subset while appearing complete. Use a scalar field as a leg.`
14466
+ });
14467
+ }
14468
+ if (field.fieldType === "reference" && field.cardinality === "many") {
14469
+ findings.push({
14470
+ severity: "error",
14471
+ rule: "lookup-composite-leg-multi-valued",
14472
+ message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but it is a reference with cardinality 'many'. One sort-key position holds one value, so a multi-valued leg would index only one target and the conjunction would silently return a subset. Use a cardinality 'one' reference, or a scalar field.`
14473
+ });
14474
+ }
14475
+ if (rangeEnabledFieldNames.has(leg)) {
14476
+ findings.push({
14477
+ severity: "error",
14478
+ rule: "lookup-composite-leg-range-enabled",
14479
+ message: `schema '${schema.typeName}': composite lookupField ${id} uses '${leg}' as a leg, but '${leg}' has its own lookupField declaring rangeEnabled \u2014 that stores it as an ordered row keyed on its own value, not an equality index. A leg is matched exactly. Declare the composite over non-range fields, and keep the range lookup as its own declaration.`
14480
+ });
14481
+ }
14482
+ if (lf.sortBy !== void 0 && leg === lf.sortBy) {
14483
+ findings.push({
14484
+ severity: "error",
14485
+ rule: "lookup-composite-leg-is-sortby",
14486
+ message: `schema '${schema.typeName}': composite lookupField ${id} cannot use '${leg}' as both a leg and its sortBy target. Within a fixed tuple every matching record has the same value for that leg, so the sort payload would be constant and a sortFrom/sortTo window would return either everything or nothing \u2014 at double the sort-key cost. Sort by a different field, 'createdAt', or 'lastUpdated'.`
14487
+ });
14488
+ }
14489
+ }
14490
+ return findings;
14236
14491
  }
14237
14492
  function lintBlueprint(bp) {
14238
14493
  const findings = [];
@@ -14240,6 +14495,7 @@ function lintBlueprint(bp) {
14240
14495
  for (const schema of bp.schemas) {
14241
14496
  typeNames.add(schema.typeName);
14242
14497
  const fieldIds = /* @__PURE__ */ new Set();
14498
+ const fieldsById = /* @__PURE__ */ new Map();
14243
14499
  for (const f of schema.fields) {
14244
14500
  if (fieldIds.has(f.fieldId)) {
14245
14501
  findings.push({
@@ -14249,6 +14505,7 @@ function lintBlueprint(bp) {
14249
14505
  });
14250
14506
  }
14251
14507
  fieldIds.add(f.fieldId);
14508
+ fieldsById.set(f.fieldId, f);
14252
14509
  if (f.enumValues && f.enumValues.length > 0 && !ENUM_COMPATIBLE_TYPES.has(f.fieldType)) {
14253
14510
  findings.push({
14254
14511
  severity: "warning",
@@ -14257,14 +14514,35 @@ function lintBlueprint(bp) {
14257
14514
  });
14258
14515
  }
14259
14516
  }
14517
+ const rangeEnabledFieldNames = /* @__PURE__ */ new Set();
14260
14518
  for (const lf of schema.lookupFields ?? []) {
14261
- const name = lookupFieldName(lf);
14262
- if (!fieldIds.has(name)) {
14263
- findings.push({
14264
- severity: "error",
14265
- rule: "lookup-field-unknown",
14266
- message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
14267
- });
14519
+ if (typeof lf !== "string" && lf.fieldName !== void 0 && lf.rangeEnabled) {
14520
+ rangeEnabledFieldNames.add(lf.fieldName);
14521
+ }
14522
+ }
14523
+ for (const lf of schema.lookupFields ?? []) {
14524
+ for (const name of lookupFieldNames(lf)) {
14525
+ if (!fieldIds.has(name)) {
14526
+ findings.push({
14527
+ severity: "error",
14528
+ rule: "lookup-field-unknown",
14529
+ message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
14530
+ });
14531
+ }
14532
+ }
14533
+ if (typeof lf !== "string" && lf.fieldNames !== void 0) {
14534
+ findings.push(
14535
+ ...compositeLegFindings(schema, { fieldNames: lf.fieldNames, sortBy: lf.sortBy }, fieldsById, rangeEnabledFieldNames)
14536
+ );
14537
+ }
14538
+ if (typeof lf !== "string" && lf.sortBy !== void 0) {
14539
+ if (!BUILT_IN_SORT_BY.has(lf.sortBy) && !fieldIds.has(lf.sortBy)) {
14540
+ findings.push({
14541
+ severity: "warning",
14542
+ rule: "lookup-sortby-unknown-field",
14543
+ message: `schema '${schema.typeName}': lookupField sortBy '${lf.sortBy}' is not a declared field (nor 'createdAt'/'lastUpdated'). The platform does not reject this \u2014 it silently treats the sort target as an untyped field no record ever populates, which usually isn't what was intended. Likely a typo.`
14544
+ });
14545
+ }
14268
14546
  }
14269
14547
  }
14270
14548
  }
@@ -14280,6 +14558,10 @@ function lintBlueprint(bp) {
14280
14558
  const ds = bp.accessProfile.dataScope;
14281
14559
  if (ds) {
14282
14560
  for (const [key, values] of Object.entries(ds)) {
14561
+ for (const value of values) {
14562
+ const bad = placeholderFinding("accessProfile", key, value);
14563
+ if (bad) findings.push(bad);
14564
+ }
14283
14565
  if (values.length > 0 && !values.includes(null)) {
14284
14566
  findings.push({
14285
14567
  severity: "warning",
@@ -14303,6 +14585,10 @@ function lintBlueprint(bp) {
14303
14585
  const cds = clause.dataScope;
14304
14586
  if (!cds) return;
14305
14587
  for (const [key, values] of Object.entries(cds)) {
14588
+ for (const value of values) {
14589
+ const bad = placeholderFinding(`role '${roleId}' clause ${i}`, key, value);
14590
+ if (bad) findings.push(bad);
14591
+ }
14306
14592
  if (values.length > 0 && !values.includes(null)) {
14307
14593
  findings.push({
14308
14594
  severity: "warning",
@@ -14316,6 +14602,28 @@ function lintBlueprint(bp) {
14316
14602
  return findings;
14317
14603
  }
14318
14604
  var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
14605
+ var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
14606
+ var RECOGNIZED_PLACEHOLDER_RES = [/^\$\{\{\s*any\s*\}\}$/, SELF_PLACEHOLDER_RE, UNDER_SELF_PLACEHOLDER_RE];
14607
+ var BLUEPRINT_TOKEN_RE = /^\s*\$\{\{\s*(identities|inputs|vectros|ns)\./;
14608
+ function placeholderFinding(where, key, value) {
14609
+ if (typeof value !== "string" || !PLACEHOLDER_SHAPED_RE.test(value)) return null;
14610
+ if (BLUEPRINT_TOKEN_RE.test(value)) return null;
14611
+ if (RECOGNIZED_PLACEHOLDER_RES.some((re) => re.test(value))) {
14612
+ const ns = placeholderScopeNamespace(value);
14613
+ const nsError = ns === null ? null : namespaceGrammarError(ns);
14614
+ if (nsError === null) return null;
14615
+ return {
14616
+ severity: "error",
14617
+ rule: "datascope-unknown-placeholder",
14618
+ message: `${where}: dataScope.${key} value '${value}' names a namespace the platform rejects \u2014 ${nsError}. It will be rejected at apply.`
14619
+ };
14620
+ }
14621
+ return {
14622
+ severity: "error",
14623
+ rule: "datascope-unknown-placeholder",
14624
+ message: `${where}: dataScope.${key} value '${value}' is not a placeholder the platform recognizes \u2014 it will be rejected at apply. Valid: \${{ any }}, \${{ self.userId }}, \${{ self.scope.<namespace> }}, \${{ under.self.userId }}, \${{ under.self.scope.<namespace> }}. Spelling is exact and lowercase; use a plain value for a literal owner id.`
14625
+ };
14626
+ }
14319
14627
  function lintPasses(findings) {
14320
14628
  return !findings.some((f) => f.severity === "error");
14321
14629
  }
@@ -14719,11 +15027,14 @@ function parseScopeOverrides(scopes) {
14719
15027
  throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
14720
15028
  }
14721
15029
  const ns = raw.slice(0, sep);
15030
+ const value = raw.slice(sep + 1);
15031
+ const valueError = valueGrammarError(value);
15032
+ if (valueError) throw new Error(`--scope value ${valueError}`);
14722
15033
  const key = `scope:${ns}`;
14723
15034
  if (Object.prototype.hasOwnProperty.call(out, key)) {
14724
15035
  throw new Error(`--scope sets namespace '${ns}' more than once \u2014 a scope override takes at most one value per namespace.`);
14725
15036
  }
14726
- out[key] = raw.slice(sep + 1);
15037
+ out[key] = value;
14727
15038
  }
14728
15039
  if (Object.keys(out).length > 2) {
14729
15040
  throw new Error("--scope allows at most two distinct namespaces.");
@@ -15068,6 +15379,7 @@ async function createIdentity(client, opts, payload) {
15068
15379
  }
15069
15380
  });
15070
15381
  }
15382
+ assertScopeEntries(opts.scope, "--scope");
15071
15383
  return client.identity.createEntity({
15072
15384
  namespace: opts.type,
15073
15385
  body: {
@@ -15273,6 +15585,7 @@ async function runAccessGrant(opts, deps = {}) {
15273
15585
  ...parseScopeOverrides(opts.scope) ?? {},
15274
15586
  ...opts.identityOverrides ? parseJsonObjectFlag("--identity-overrides", opts.identityOverrides) : {}
15275
15587
  };
15588
+ assertIdentityOverrideValues(identityOverrides, "--identity-overrides");
15276
15589
  } catch (err) {
15277
15590
  process.stderr.write(` \u2716 ${formatError(err)}
15278
15591
  `);
@@ -15425,30 +15738,21 @@ ${renderProfile(p)}
15425
15738
 
15426
15739
  // src/commands/access-explain.ts
15427
15740
  var SELF = /* @__PURE__ */ Symbol("self");
15741
+ var ANY = /* @__PURE__ */ Symbol("any");
15742
+ var UNDER_SELF = /* @__PURE__ */ Symbol("under.self");
15428
15743
  function canonicalDim(key) {
15429
15744
  if (key === "userId" || key === "partnerUserId") return "user";
15430
15745
  return key;
15431
15746
  }
15432
- var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
15433
- "user",
15434
- "record",
15435
- "document",
15436
- "entity",
15437
- "self",
15438
- "tenant",
15439
- "context",
15440
- "scope",
15441
- "versions",
15442
- "lookup"
15443
- ]);
15444
- var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
15445
15747
  function isRecognizedOwnershipKey(key) {
15748
+ if (key === DIM_WILDCARD) return true;
15446
15749
  if (key === "userId" || key === "partnerUserId") return true;
15447
15750
  if (!key.startsWith("scope:")) return false;
15448
15751
  const ns = key.slice("scope:".length);
15449
15752
  return !FORBIDDEN_SCOPE_NAMESPACES.has(ns) && SCOPE_NAMESPACE_RE.test(ns);
15450
15753
  }
15451
- var SELF_TOKEN_RE2 = /^\$\{\{\s*self\.[a-zA-Z.]+\s*\}\}$/;
15754
+ var DIM_WILDCARD = "*";
15755
+ var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
15452
15756
  function normalizeClauses(raw) {
15453
15757
  const out = [];
15454
15758
  for (const c of raw) {
@@ -15464,7 +15768,9 @@ function normalizeClauses(raw) {
15464
15768
  dataScope[dim] = values.map((val) => {
15465
15769
  if (val === null) return null;
15466
15770
  const s = String(val);
15467
- return SELF_TOKEN_RE2.test(s) ? SELF : s;
15771
+ if (ANY_TOKEN_RE.test(s)) return ANY;
15772
+ if (UNDER_SELF_PLACEHOLDER_RE.test(s)) return UNDER_SELF;
15773
+ return SELF_PLACEHOLDER_RE.test(s) ? SELF : s;
15468
15774
  });
15469
15775
  if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
15470
15776
  }
@@ -15472,6 +15778,38 @@ function normalizeClauses(raw) {
15472
15778
  }
15473
15779
  return out;
15474
15780
  }
15781
+ function recordTypesOf(actions) {
15782
+ if (actions.includes("*")) return "all";
15783
+ const types = /* @__PURE__ */ new Set();
15784
+ let sawRecordsAction = false;
15785
+ let sawUntyped = false;
15786
+ for (const action of actions) {
15787
+ const parts = action.split(":");
15788
+ if (parts[0] !== "records") continue;
15789
+ if (!(parts[1] ?? "").includes("r")) continue;
15790
+ if (parts.length >= 3) {
15791
+ const type = parts.slice(2).join(":");
15792
+ if (type === "") continue;
15793
+ if (type === "*") continue;
15794
+ if (type.endsWith(":*")) {
15795
+ const stripped = type.slice(0, -2);
15796
+ if (stripped === "" || stripped.includes(":")) continue;
15797
+ sawRecordsAction = true;
15798
+ types.add(stripped);
15799
+ continue;
15800
+ }
15801
+ if (type.includes(":")) continue;
15802
+ sawRecordsAction = true;
15803
+ types.add(type);
15804
+ } else {
15805
+ sawRecordsAction = true;
15806
+ sawUntyped = true;
15807
+ }
15808
+ }
15809
+ if (!sawRecordsAction) return null;
15810
+ if (sawUntyped) return "all";
15811
+ return [...types].sort();
15812
+ }
15475
15813
  function buildArchetypes(clauses) {
15476
15814
  const rows = [
15477
15815
  { label: "a record you own alone (private)", owners: { user: SELF } },
@@ -15482,8 +15820,13 @@ function buildArchetypes(clauses) {
15482
15820
  if (c.unrecognizedDims.length > 0) continue;
15483
15821
  for (const [dim, values] of Object.entries(c.dataScope)) {
15484
15822
  if (dim === "user") continue;
15823
+ if (dim === DIM_WILDCARD) continue;
15485
15824
  const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
15486
- for (const v of values) if (v !== null) set.add(v);
15825
+ set.add(SELF);
15826
+ for (const v of values) {
15827
+ if (v === null || v === ANY || v === UNDER_SELF) continue;
15828
+ set.add(v);
15829
+ }
15487
15830
  scopeDims.set(dim, set);
15488
15831
  }
15489
15832
  }
@@ -15502,19 +15845,42 @@ function buildArchetypes(clauses) {
15502
15845
  rows.push({ label: "no owner values at all (tenant-shared)", owners: {} });
15503
15846
  return rows;
15504
15847
  }
15848
+ function listAdmits(allowed, recordValue) {
15849
+ return allowed.some((a) => {
15850
+ if (a === null) return recordValue === void 0;
15851
+ if (a === SELF) return recordValue === SELF;
15852
+ if (a === ANY) return recordValue !== void 0;
15853
+ if (a === UNDER_SELF) return false;
15854
+ return recordValue === a;
15855
+ });
15856
+ }
15505
15857
  function clauseMatches(clause, arch) {
15506
15858
  if (clause.unrecognizedDims.length > 0) return false;
15859
+ const wildcard = clause.dataScope[DIM_WILDCARD];
15507
15860
  for (const [dim, allowed] of Object.entries(clause.dataScope)) {
15508
- const recordValue = arch.owners[dim];
15509
- const ok = allowed.some((a) => {
15510
- if (a === null) return recordValue === void 0;
15511
- if (a === SELF) return recordValue === SELF;
15512
- return recordValue === a;
15513
- });
15514
- if (!ok) return false;
15861
+ if (dim === DIM_WILDCARD) continue;
15862
+ if (!listAdmits(allowed, arch.owners[dim])) return false;
15863
+ }
15864
+ if (wildcard) {
15865
+ let carriesAny = false;
15866
+ for (const [dim, value] of Object.entries(arch.owners)) {
15867
+ carriesAny = true;
15868
+ if (dim in clause.dataScope) continue;
15869
+ if (!listAdmits(wildcard, value)) return false;
15870
+ }
15871
+ if (!carriesAny && !listAdmits(wildcard, void 0)) return false;
15515
15872
  }
15516
15873
  return true;
15517
15874
  }
15875
+ function undecidableDims(clauses) {
15876
+ const dims = /* @__PURE__ */ new Set();
15877
+ for (const c of clauses) {
15878
+ for (const [dim, values] of Object.entries(c.dataScope)) {
15879
+ if (values.includes(UNDER_SELF)) dims.add(dim);
15880
+ }
15881
+ }
15882
+ return [...dims];
15883
+ }
15518
15884
  function explainClauses(clauses) {
15519
15885
  const archetypes = buildArchetypes(clauses);
15520
15886
  return archetypes.map((arch) => {
@@ -15526,15 +15892,34 @@ function explainClauses(clauses) {
15526
15892
  for (const a of c.actions) actions.add(a);
15527
15893
  }
15528
15894
  });
15529
- return { archetype: arch.label, clauses: matched, actions: [...actions] };
15895
+ const recordTypes = recordTypesOf([...actions]);
15896
+ return {
15897
+ archetype: arch.label,
15898
+ clauses: matched,
15899
+ actions: [...actions],
15900
+ ...recordTypes !== null ? { recordTypes } : {}
15901
+ };
15530
15902
  });
15531
15903
  }
15532
15904
  function renderDataScope(ds) {
15533
15905
  const entries = Object.entries(ds);
15534
15906
  if (entries.length === 0) return "(unscoped \u2014 all data in the context)";
15535
- return entries.map(
15536
- ([dim, values]) => `${dim}:[${values.map((v) => v === SELF ? "self" : v === null ? "null" : v).join(", ")}]`
15537
- ).join(" AND ");
15907
+ return entries.map(([dim, values]) => {
15908
+ const shown = values.map((v) => {
15909
+ if (v === SELF) return "self";
15910
+ if (v === ANY) return "any";
15911
+ if (v === UNDER_SELF) return "under-self";
15912
+ if (v === null) return "null";
15913
+ return v;
15914
+ });
15915
+ const label = dim === DIM_WILDCARD ? "* (every other dimension)" : dim;
15916
+ return `${label}:[${shown.join(", ")}]`;
15917
+ }).join(" AND ");
15918
+ }
15919
+ function renderRecordTypes(recordTypes) {
15920
+ if (recordTypes === void 0) return "\u2014";
15921
+ if (recordTypes === "all") return "all types";
15922
+ return recordTypes.join(", ");
15538
15923
  }
15539
15924
  var SELF_PRINCIPAL_ALIAS2 = "me";
15540
15925
  async function runAccessExplain(opts, deps = {}) {
@@ -15562,6 +15947,7 @@ async function runAccessExplain(opts, deps = {}) {
15562
15947
  }
15563
15948
  const clauses = normalizeClauses(rawClauses);
15564
15949
  const rows = explainClauses(clauses);
15950
+ const undecidable = undecidableDims(clauses);
15565
15951
  if (opts.json) {
15566
15952
  process.stdout.write(
15567
15953
  `${formatJson({
@@ -15578,7 +15964,10 @@ async function runAccessExplain(opts, deps = {}) {
15578
15964
  ...c.unrecognizedDims.length > 0 ? { grantsNothing: true, unrecognizedDims: c.unrecognizedDims } : {}
15579
15965
  })),
15580
15966
  matrix: rows,
15581
- note: "Ownership dimensions only: per-type action qualifiers narrow further, and search/RAG visibility is bounded per row by the same clause's typed read grants. Client-side preview; the platform decides at request time."
15967
+ // Present only when a clause uses `${{ under.self.* }}`: those rows are UNDECIDED
15968
+ // offline, not denied, and a consumer reading a blank cell as a denial would be wrong.
15969
+ ...undecidable.length > 0 ? { undecidableDims: undecidable } : {},
15970
+ note: "This preview models READ reachability, not write authorization: a `c`/`u`/`d` verb shown granted for a row may not actually be able to create/update a record with that exact ownership shape, because the platform requires a clause to explicitly name a dimension to place a value into it, while an unmentioned dimension never narrows reads. matrix[].recordTypes is the `records:` reach of that row's grant, separate from the ownership archetype: \"all\" reaches every type, an array names the type(s), absent means no `records:` action grants this row. Search/RAG visibility is a separate, still-unmodeled narrowing bounded per row by the same clause's typed read grants. Client-side preview; the platform decides at request time."
15582
15971
  })}
15583
15972
  `
15584
15973
  );
@@ -15606,8 +15995,19 @@ async function runAccessExplain(opts, deps = {}) {
15606
15995
  "\n" + formatTable(rows, [
15607
15996
  { header: "record owned by\u2026", get: (r) => r.archetype },
15608
15997
  { header: "granted?", get: (r) => r.clauses.length ? `via clause ${r.clauses.join(", ")}` : "NO" },
15609
- { header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" }
15610
- ]) + "\n\n NOTE: this matrix models the OWNERSHIP dimensions only. Per-type qualifiers narrow further:\n a `records:<ops>:<type>` action reaches only that type, and search/RAG visibility is bounded\n per row by the SAME clause's typed read grants \u2014 a row shown with `search:r` is searchable\n only for the content types that clause can read.\n Preview only \u2014 computed client-side from the documented matching rules; the platform decides at request time.\n"
15998
+ { header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" },
15999
+ // The ownership "granted?" cell says nothing about WHICH record types — a clause can be
16000
+ // ownership-unscoped and still type-restricted (a curated shared-KB clause with no owner
16001
+ // dimension, readable only for a few named types, never others). Surface the actual
16002
+ // `records:` reach per row instead of leaving it to a footnote.
16003
+ { header: "record types", get: (r) => renderRecordTypes(r.recordTypes) }
16004
+ ]) + (undecidable.length > 0 ? `
16005
+
16006
+ \u26A0 UNDECIDED offline: ${undecidable.join(", ")}
16007
+ A clause matching on \`\${{ under.self.* }}\` needs the parent graph, which only the server has,
16008
+ so rows granted through it are shown as NO above. Read those cells as "cannot tell here",
16009
+ NOT as a denial \u2014 the matrix under-reports this clause.
16010
+ ` : "") + '\n\n \u26A0 READ ONLY: "granted?"/"actions" model whether a row can be READ, not whether a c/u/d\n verb shown there can actually WRITE that row\'s exact shape \u2014 the platform requires a clause\n to name a dimension to place a value into it, while reads are never narrowed by a dimension a\n clause omits. A create/update verb next to a row is advisory: it names which records exist to\n act on, not that a specific write will succeed.\n\n\n NOTE: this matrix models the OWNERSHIP dimensions only \u2014 "granted?" says a row\'s ownership\n shape matches a clause, and "record types" (above) says which `records:` types that grant\n actually reaches; "all" reaches every type, "\u2014" means no `records:` action grants this row at\n all. Search/RAG visibility is a separate, still-unmodeled narrowing: it is bounded per row by\n the SAME clause\'s typed read grants \u2014 a row shown with `search:r` is searchable only for the\n content types that clause can read.\n Preview only \u2014 computed client-side from the documented matching rules; the platform decides at request time.\n'
15611
16011
  );
15612
16012
  return 0;
15613
16013
  } catch (err) {
@@ -18346,9 +18746,9 @@ function emitJoined(alias, context, principalId, role, keyId, reused) {
18346
18746
 
18347
18747
  // src/build-info.ts
18348
18748
  var BUILD_INFO = {
18349
- cli: true ? "0.11.0" : "dev",
18350
- sdk: true ? "0.36.0" : "dev",
18351
- blueprints: true ? "0.8.0" : "dev"
18749
+ cli: true ? "0.13.0" : "dev",
18750
+ sdk: true ? "0.38.0" : "dev",
18751
+ blueprints: true ? "0.10.0" : "dev"
18352
18752
  };
18353
18753
  function formatBuildInfo() {
18354
18754
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;