@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.mjs CHANGED
@@ -1619,7 +1619,7 @@ var require_proper_lockfile = __commonJS({
1619
1619
  // src/cli-bootstrap.ts
1620
1620
  import * as readline from "readline/promises";
1621
1621
 
1622
- // ../../node_modules/@vectros-ai/sdk/dist/index.mjs
1622
+ // node_modules/@vectros-ai/sdk/dist/index.mjs
1623
1623
  var __defProp2 = Object.defineProperty;
1624
1624
  var __export = (target, all) => {
1625
1625
  for (var name in all)
@@ -4048,7 +4048,7 @@ var AuthClient = class {
4048
4048
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/admin/access-log");
4049
4049
  }
4050
4050
  /**
4051
- * Lists all of your scoped API keys (`ssk_*`) across both your live and test environments. Revoked keys are excluded. Requires the `keys:r` scope.
4051
+ * 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.
4052
4052
  *
4053
4053
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
4054
4054
  *
@@ -4431,7 +4431,7 @@ var AuthClient = class {
4431
4431
  );
4432
4432
  }
4433
4433
  /**
4434
- * 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.
4434
+ * 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.
4435
4435
  *
4436
4436
  * @param {Vectros.CreateAccessProfileRequest} request
4437
4437
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4849,7 +4849,7 @@ var AuthClient = class {
4849
4849
  );
4850
4850
  }
4851
4851
  /**
4852
- * 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.
4852
+ * 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.
4853
4853
  *
4854
4854
  * @param {Vectros.UpdateAccessProfileRequest} request
4855
4855
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -4922,7 +4922,7 @@ var AuthClient = class {
4922
4922
  );
4923
4923
  }
4924
4924
  /**
4925
- * 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.
4925
+ * 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.
4926
4926
  *
4927
4927
  * @param {Vectros.DeleteAccessProfileRequest} request
4928
4928
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5776,7 +5776,7 @@ var AuthClient = class {
5776
5776
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/auth/token");
5777
5777
  }
5778
5778
  /**
5779
- * 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.
5779
+ * 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.
5780
5780
  *
5781
5781
  * @param {Vectros.CreateInviteRequest} request
5782
5782
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -5847,7 +5847,7 @@ var AuthClient = class {
5847
5847
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "POST", "/v1/users/invite");
5848
5848
  }
5849
5849
  /**
5850
- * 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.
5850
+ * 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.
5851
5851
  *
5852
5852
  * @param {Vectros.CreateInviteRequest} request
5853
5853
  * @param {AuthClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6181,7 +6181,7 @@ var DocumentsClient = class {
6181
6181
  * userId: "550e8400-e29b-41d4-a716-446655440000",
6182
6182
  * scope: "group:eng-team",
6183
6183
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
6184
- * startFrom: "doc_prev123"
6184
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
6185
6185
  * })
6186
6186
  */
6187
6187
  listDocuments(request = {}, requestOptions) {
@@ -6229,7 +6229,7 @@ var DocumentsClient = class {
6229
6229
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents");
6230
6230
  }
6231
6231
  /**
6232
- * 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.
6232
+ * 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.
6233
6233
  *
6234
6234
  * @param {Vectros.IngestDocumentRequest} request
6235
6235
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -6491,6 +6491,7 @@ var DocumentsClient = class {
6491
6491
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
6492
6492
  *
6493
6493
  * @throws {@link Vectros.BadRequestError}
6494
+ * @throws {@link Vectros.ForbiddenError}
6494
6495
  * @throws {@link Vectros.NotFoundError}
6495
6496
  * @throws {@link Vectros.ConflictError}
6496
6497
  * @throws {@link Vectros.TooManyRequestsError}
@@ -6538,6 +6539,8 @@ var DocumentsClient = class {
6538
6539
  switch (_response.error.statusCode) {
6539
6540
  case 400:
6540
6541
  throw new BadRequestError(_response.error.body, _response.rawResponse);
6542
+ case 403:
6543
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
6541
6544
  case 404:
6542
6545
  throw new NotFoundError(_response.error.body, _response.rawResponse);
6543
6546
  case 409:
@@ -6555,25 +6558,29 @@ var DocumentsClient = class {
6555
6558
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PATCH", "/v1/documents/{id}");
6556
6559
  }
6557
6560
  /**
6558
- * 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.
6561
+ * 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.
6559
6562
  *
6560
6563
  * @param {Vectros.LookupDocumentsRequest} request
6561
6564
  * @param {DocumentsClient.RequestOptions} requestOptions - Request-specific configuration.
6562
6565
  *
6566
+ * @throws {@link Vectros.BadRequestError}
6567
+ *
6563
6568
  * @example
6564
6569
  * await client.documents.lookupDocuments({
6565
6570
  * type: "invoice",
6566
6571
  * field: "po_number",
6567
6572
  * value: "PO-1001",
6568
6573
  * prefix: "PO-2024",
6569
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
6574
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
6575
+ * userId: "550e8400-e29b-41d4-a716-446655440000",
6576
+ * scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
6570
6577
  * })
6571
6578
  */
6572
6579
  lookupDocuments(request, requestOptions) {
6573
6580
  return HttpResponsePromise.fromPromise(this.__lookupDocuments(request, requestOptions));
6574
6581
  }
6575
6582
  async __lookupDocuments(request, requestOptions) {
6576
- const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, order } = request;
6583
+ const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, order, userId, scope } = request;
6577
6584
  const _queryParams = {
6578
6585
  type: type_,
6579
6586
  field,
@@ -6583,7 +6590,9 @@ var DocumentsClient = class {
6583
6590
  prefix,
6584
6591
  startFrom,
6585
6592
  limit,
6586
- order: order != null ? order : void 0
6593
+ order: order != null ? order : void 0,
6594
+ userId,
6595
+ scope
6587
6596
  };
6588
6597
  const _authRequest = await this._options.authProvider.getAuthRequest();
6589
6598
  const _headers = mergeHeaders(
@@ -6609,11 +6618,16 @@ var DocumentsClient = class {
6609
6618
  return { data: _response.body, rawResponse: _response.rawResponse };
6610
6619
  }
6611
6620
  if (_response.error.reason === "status-code") {
6612
- throw new VectrosError({
6613
- statusCode: _response.error.statusCode,
6614
- body: _response.error.body,
6615
- rawResponse: _response.rawResponse
6616
- });
6621
+ switch (_response.error.statusCode) {
6622
+ case 400:
6623
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
6624
+ default:
6625
+ throw new VectrosError({
6626
+ statusCode: _response.error.statusCode,
6627
+ body: _response.error.body,
6628
+ rawResponse: _response.rawResponse
6629
+ });
6630
+ }
6617
6631
  }
6618
6632
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/documents/lookup");
6619
6633
  }
@@ -6933,7 +6947,7 @@ var FoldersClient = class {
6933
6947
  * parentFolderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
6934
6948
  * userId: "550e8400-e29b-41d4-a716-446655440000",
6935
6949
  * scope: "group:eng-team",
6936
- * startFrom: "fld_prev123"
6950
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
6937
6951
  * })
6938
6952
  */
6939
6953
  listFolders(request = {}, requestOptions) {
@@ -6981,7 +6995,7 @@ var FoldersClient = class {
6981
6995
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/folders");
6982
6996
  }
6983
6997
  /**
6984
- * 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.
6998
+ * 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.
6985
6999
  *
6986
7000
  * @param {Vectros.CreateFolderRequest} request
6987
7001
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7110,6 +7124,7 @@ var FoldersClient = class {
7110
7124
  * @param {Vectros.UpdateFolderRequest} request
7111
7125
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
7112
7126
  *
7127
+ * @throws {@link Vectros.ForbiddenError}
7113
7128
  * @throws {@link Vectros.NotFoundError}
7114
7129
  * @throws {@link Vectros.TooManyRequestsError}
7115
7130
  *
@@ -7154,6 +7169,8 @@ var FoldersClient = class {
7154
7169
  }
7155
7170
  if (_response.error.reason === "status-code") {
7156
7171
  switch (_response.error.statusCode) {
7172
+ case 403:
7173
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
7157
7174
  case 404:
7158
7175
  throw new NotFoundError(_response.error.body, _response.rawResponse);
7159
7176
  case 429:
@@ -7236,6 +7253,7 @@ var FoldersClient = class {
7236
7253
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
7237
7254
  *
7238
7255
  * @throws {@link Vectros.BadRequestError}
7256
+ * @throws {@link Vectros.ForbiddenError}
7239
7257
  * @throws {@link Vectros.NotFoundError}
7240
7258
  * @throws {@link Vectros.ConflictError}
7241
7259
  * @throws {@link Vectros.TooManyRequestsError}
@@ -7283,6 +7301,8 @@ var FoldersClient = class {
7283
7301
  switch (_response.error.statusCode) {
7284
7302
  case 400:
7285
7303
  throw new BadRequestError(_response.error.body, _response.rawResponse);
7304
+ case 403:
7305
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
7286
7306
  case 404:
7287
7307
  throw new NotFoundError(_response.error.body, _response.rawResponse);
7288
7308
  case 409:
@@ -7370,7 +7390,8 @@ var IdentityClient = class {
7370
7390
  *
7371
7391
  * @example
7372
7392
  * await client.identity.listEntities({
7373
- * namespace: "team"
7393
+ * namespace: "team",
7394
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7374
7395
  * })
7375
7396
  */
7376
7397
  listEntities(request, requestOptions) {
@@ -7439,7 +7460,7 @@ var IdentityClient = class {
7439
7460
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/entities/{namespace}");
7440
7461
  }
7441
7462
  /**
7442
- * 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.
7463
+ * 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.
7443
7464
  *
7444
7465
  * @param {Vectros.CreateEntityRequest} request
7445
7466
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7883,7 +7904,7 @@ var IdentityClient = class {
7883
7904
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces/{namespace}");
7884
7905
  }
7885
7906
  /**
7886
- * 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.
7907
+ * 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.
7887
7908
  *
7888
7909
  * @param {Vectros.UpdateNamespaceRequest} request
7889
7910
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -7896,7 +7917,8 @@ var IdentityClient = class {
7896
7917
  * await client.identity.updateNamespace({
7897
7918
  * namespace: "team",
7898
7919
  * body: {
7899
- * namespace: "team"
7920
+ * namespace: "team",
7921
+ * specificityRank: 1500
7900
7922
  * }
7901
7923
  * })
7902
7924
  */
@@ -8061,7 +8083,7 @@ var IdentityClient = class {
8061
8083
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/namespaces");
8062
8084
  }
8063
8085
  /**
8064
- * 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.
8086
+ * 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.
8065
8087
  *
8066
8088
  * @param {Vectros.NamespaceRequest} request
8067
8089
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8072,7 +8094,8 @@ var IdentityClient = class {
8072
8094
  *
8073
8095
  * @example
8074
8096
  * await client.identity.registerNamespace({
8075
- * namespace: "team"
8097
+ * namespace: "team",
8098
+ * specificityRank: 1500
8076
8099
  * })
8077
8100
  */
8078
8101
  registerNamespace(request, requestOptions) {
@@ -8132,7 +8155,7 @@ var IdentityClient = class {
8132
8155
  * @example
8133
8156
  * await client.identity.listUsers({
8134
8157
  * externalId: "usr_12345",
8135
- * startFrom: "550e8400-e29b-41d4-a716-446655440000",
8158
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
8136
8159
  * type: "person_v1",
8137
8160
  * field: "team",
8138
8161
  * value: "engineering",
@@ -8189,7 +8212,7 @@ var IdentityClient = class {
8189
8212
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users");
8190
8213
  }
8191
8214
  /**
8192
- * 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.
8215
+ * 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.
8193
8216
  *
8194
8217
  * @param {Vectros.CreateUserRequest} request
8195
8218
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -8313,11 +8336,12 @@ var IdentityClient = class {
8313
8336
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
8314
8337
  }
8315
8338
  /**
8316
- * 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.
8339
+ * 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.
8317
8340
  *
8318
8341
  * @param {Vectros.UpdateUserRequest} request
8319
8342
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8320
8343
  *
8344
+ * @throws {@link Vectros.BadRequestError}
8321
8345
  * @throws {@link Vectros.NotFoundError}
8322
8346
  * @throws {@link Vectros.TooManyRequestsError}
8323
8347
  *
@@ -8362,6 +8386,8 @@ var IdentityClient = class {
8362
8386
  }
8363
8387
  if (_response.error.reason === "status-code") {
8364
8388
  switch (_response.error.statusCode) {
8389
+ case 400:
8390
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
8365
8391
  case 404:
8366
8392
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8367
8393
  case 429:
@@ -9009,7 +9035,7 @@ var RecordsClient = class {
9009
9035
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
9010
9036
  * userId: "550e8400-e29b-41d4-a716-446655440000",
9011
9037
  * scope: "group:eng-team",
9012
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
9038
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9013
9039
  * })
9014
9040
  */
9015
9041
  listRecords(request = {}, requestOptions) {
@@ -9060,7 +9086,7 @@ var RecordsClient = class {
9060
9086
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/records");
9061
9087
  }
9062
9088
  /**
9063
- * 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.
9089
+ * 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.
9064
9090
  *
9065
9091
  * @param {Vectros.CreateRecordRequest} request
9066
9092
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9197,6 +9223,7 @@ var RecordsClient = class {
9197
9223
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9198
9224
  *
9199
9225
  * @throws {@link Vectros.BadRequestError}
9226
+ * @throws {@link Vectros.ForbiddenError}
9200
9227
  * @throws {@link Vectros.NotFoundError}
9201
9228
  * @throws {@link Vectros.ConflictError}
9202
9229
  * @throws {@link Vectros.TooManyRequestsError}
@@ -9245,6 +9272,8 @@ var RecordsClient = class {
9245
9272
  switch (_response.error.statusCode) {
9246
9273
  case 400:
9247
9274
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9275
+ case 403:
9276
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9248
9277
  case 404:
9249
9278
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9250
9279
  case 409:
@@ -9326,6 +9355,7 @@ var RecordsClient = class {
9326
9355
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9327
9356
  *
9328
9357
  * @throws {@link Vectros.BadRequestError}
9358
+ * @throws {@link Vectros.ForbiddenError}
9329
9359
  * @throws {@link Vectros.NotFoundError}
9330
9360
  * @throws {@link Vectros.ConflictError}
9331
9361
  * @throws {@link Vectros.TooManyRequestsError}
@@ -9376,6 +9406,8 @@ var RecordsClient = class {
9376
9406
  switch (_response.error.statusCode) {
9377
9407
  case 400:
9378
9408
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9409
+ case 403:
9410
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9379
9411
  case 404:
9380
9412
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9381
9413
  case 409:
@@ -9393,7 +9425,15 @@ var RecordsClient = class {
9393
9425
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PATCH", "/v1/records/{id}");
9394
9426
  }
9395
9427
  /**
9396
- * 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.
9428
+ * 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.
9429
+ *
9430
+ * 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`.
9431
+ *
9432
+ * 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.
9433
+ *
9434
+ * 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.
9435
+ *
9436
+ * 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.
9397
9437
  *
9398
9438
  * @param {Vectros.LookupRecordsRequest} request
9399
9439
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9405,22 +9445,40 @@ var RecordsClient = class {
9405
9445
  * type: "intake_form",
9406
9446
  * field: "email",
9407
9447
  * value: "jane@example.com",
9448
+ * values: ["open"],
9408
9449
  * prefix: "jane",
9409
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
9450
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9410
9451
  * })
9411
9452
  */
9412
9453
  lookupRecords(request, requestOptions) {
9413
9454
  return HttpResponsePromise.fromPromise(this.__lookupRecords(request, requestOptions));
9414
9455
  }
9415
9456
  async __lookupRecords(request, requestOptions) {
9416
- const { type: type_, field, value, from: from_, to, prefix, startFrom, limit, includePayload, order } = request;
9457
+ const {
9458
+ type: type_,
9459
+ field,
9460
+ value,
9461
+ values,
9462
+ from: from_,
9463
+ to,
9464
+ prefix,
9465
+ sortFrom,
9466
+ sortTo,
9467
+ startFrom,
9468
+ limit,
9469
+ includePayload,
9470
+ order
9471
+ } = request;
9417
9472
  const _queryParams = {
9418
9473
  type: type_,
9419
9474
  field,
9420
9475
  value,
9476
+ values,
9421
9477
  from: from_,
9422
9478
  to,
9423
9479
  prefix,
9480
+ sortFrom,
9481
+ sortTo,
9424
9482
  startFrom,
9425
9483
  limit,
9426
9484
  includePayload,
@@ -9464,7 +9522,7 @@ var RecordsClient = class {
9464
9522
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/records/lookup");
9465
9523
  }
9466
9524
  /**
9467
- * 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.
9525
+ * 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.
9468
9526
  *
9469
9527
  * @param {Vectros.RecordLookupRequest} request
9470
9528
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9651,13 +9709,15 @@ var SchemasClient = class {
9651
9709
  * @param {Vectros.ListSchemasRequest} request
9652
9710
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
9653
9711
  *
9712
+ * @throws {@link Vectros.BadRequestError}
9713
+ *
9654
9714
  * @example
9655
9715
  * await client.schemas.listSchemas({
9656
9716
  * userId: "550e8400-e29b-41d4-a716-446655440000",
9657
9717
  * scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8",
9658
9718
  * surface: "document",
9659
9719
  * recordType: "intake_form",
9660
- * startFrom: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
9720
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9661
9721
  * })
9662
9722
  */
9663
9723
  listSchemas(request = {}, requestOptions) {
@@ -9697,16 +9757,21 @@ var SchemasClient = class {
9697
9757
  return { data: _response.body, rawResponse: _response.rawResponse };
9698
9758
  }
9699
9759
  if (_response.error.reason === "status-code") {
9700
- throw new VectrosError({
9701
- statusCode: _response.error.statusCode,
9702
- body: _response.error.body,
9703
- rawResponse: _response.rawResponse
9704
- });
9760
+ switch (_response.error.statusCode) {
9761
+ case 400:
9762
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
9763
+ default:
9764
+ throw new VectrosError({
9765
+ statusCode: _response.error.statusCode,
9766
+ body: _response.error.body,
9767
+ rawResponse: _response.rawResponse
9768
+ });
9769
+ }
9705
9770
  }
9706
9771
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/schemas");
9707
9772
  }
9708
9773
  /**
9709
- * 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.
9774
+ * 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.
9710
9775
  *
9711
9776
  * @param {Vectros.CreateSchemaRequest} request
9712
9777
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9850,12 +9915,13 @@ var SchemasClient = class {
9850
9915
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/schemas/{id}");
9851
9916
  }
9852
9917
  /**
9853
- * 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.
9918
+ * 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.
9854
9919
  *
9855
9920
  * @param {Vectros.UpdateSchemaRequest} request
9856
9921
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
9857
9922
  *
9858
9923
  * @throws {@link Vectros.BadRequestError}
9924
+ * @throws {@link Vectros.ForbiddenError}
9859
9925
  * @throws {@link Vectros.NotFoundError}
9860
9926
  * @throws {@link Vectros.TooManyRequestsError}
9861
9927
  *
@@ -9904,6 +9970,8 @@ var SchemasClient = class {
9904
9970
  switch (_response.error.statusCode) {
9905
9971
  case 400:
9906
9972
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9973
+ case 403:
9974
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9907
9975
  case 404:
9908
9976
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9909
9977
  case 429:
@@ -9919,7 +9987,7 @@ var SchemasClient = class {
9919
9987
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PUT", "/v1/schemas/{id}");
9920
9988
  }
9921
9989
  /**
9922
- * 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.
9990
+ * 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.
9923
9991
  *
9924
9992
  * @param {Vectros.DeleteSchemaRequest} request
9925
9993
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -10170,7 +10238,7 @@ var logging;
10170
10238
  logging2.ConsoleLogger = ConsoleLogger;
10171
10239
  })(logging || (logging = {}));
10172
10240
 
10173
- // ../blueprints/dist/index.mjs
10241
+ // node_modules/@vectros-ai/blueprints/dist/index.mjs
10174
10242
  import { z } from "zod";
10175
10243
  import { z as z2 } from "zod";
10176
10244
  var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
@@ -10274,19 +10342,63 @@ var BlueprintFieldDefSchema = z.object({
10274
10342
  var BlueprintLookupFieldSchema = z.union([
10275
10343
  z.string().min(1),
10276
10344
  z.object({
10277
- fieldName: z.string().min(1),
10345
+ // A lookup field declares either ONE field (fieldName) or 2-3 fields
10346
+ // matched together (fieldNames) — never both, never neither. `.strict()`
10347
+ // below only rejects unknown KEYS; it does not enforce this combination,
10348
+ // so the `.superRefine()` after it carries the actual rule.
10349
+ fieldName: z.string().min(1).optional(),
10350
+ // Composite (conjunctive) lookup: match on all of these fields at once —
10351
+ // "every record where `status` is `open` AND `area` is `billing`", exact,
10352
+ // in a stable order. Order is significant and locked at create: a query
10353
+ // may match a leading run of the list (the first field alone, the first
10354
+ // two together, …) but never a later field alone — declare a separate
10355
+ // lookup for that. Record-only: the schema's `allowedSurfaces` must be
10356
+ // exactly `['record']` (checked at the schema level below) — the
10357
+ // platform's composite index has no document/user/entity reader yet.
10358
+ fieldNames: z.array(z.string().min(1)).min(2).max(3).optional(),
10359
+ // Not available on a composite (`fieldNames`) lookup — checked below.
10278
10360
  unique: z.boolean().optional(),
10279
10361
  // Opt this field into ordered range + prefix lookups (from/to/prefix) on
10280
10362
  // top of exact match. Billed at the range-index rate; not valid on a
10281
- // sensitive field (a blind index is not orderable). Locked at create.
10363
+ // sensitive field (a blind index is not orderable), and — like `unique`
10364
+ // — not available on a composite lookup: it is an exact-match index over
10365
+ // its fields; declare the range lookup separately. Locked at create.
10282
10366
  rangeEnabled: z.boolean().optional(),
10283
10367
  // Sort key for the exact-match index: 'createdAt' (default), 'lastUpdated',
10284
- // or a declared field on this schema. Locked at create.
10368
+ // or a declared field on this schema. Locked at create. Valid on a
10369
+ // composite lookup too — it orders WITHIN a group when a query supplies
10370
+ // fewer values than the lookup declares, not across the whole result.
10285
10371
  sortBy: z.string().min(1).optional(),
10286
10372
  // Opt a field past the fixed fast-index budget into a higher-cost
10287
10373
  // secondary index. No effect on a field that fits within the budget.
10288
10374
  allowOverflow: z.boolean().optional()
10289
- }).strict()
10375
+ }).strict().superRefine((lf, ctx) => {
10376
+ const hasFieldName = lf.fieldName !== void 0;
10377
+ const hasFieldNames = lf.fieldNames !== void 0;
10378
+ if (hasFieldName === hasFieldNames) {
10379
+ ctx.addIssue({
10380
+ code: z.ZodIssueCode.custom,
10381
+ message: "a lookup field declares either 'fieldName' (one field) or 'fieldNames' (2-3 fields matched together), never both and never neither"
10382
+ });
10383
+ return;
10384
+ }
10385
+ if (hasFieldNames) {
10386
+ if (lf.unique !== void 0) {
10387
+ ctx.addIssue({
10388
+ code: z.ZodIssueCode.custom,
10389
+ path: ["unique"],
10390
+ message: "'unique' is not available on a composite ('fieldNames') lookup"
10391
+ });
10392
+ }
10393
+ if (lf.rangeEnabled !== void 0) {
10394
+ ctx.addIssue({
10395
+ code: z.ZodIssueCode.custom,
10396
+ path: ["rangeEnabled"],
10397
+ message: "'rangeEnabled' is not available on a composite ('fieldNames') lookup"
10398
+ });
10399
+ }
10400
+ }
10401
+ })
10290
10402
  ]);
10291
10403
  var BlueprintCapabilitiesSchema = z.object({
10292
10404
  auditHistory: z.boolean().optional()
@@ -10316,8 +10428,29 @@ var BlueprintSchemaSchema = z.object({
10316
10428
  // With a scoped token these must be consistent with the profile's dataScope
10317
10429
  // (a cross-consistency lint is deferred to the lint slice).
10318
10430
  userId: z.string().min(1).optional(),
10319
- scopes: z.array(z.string().min(1)).max(2).optional()
10320
- }).strict();
10431
+ scopes: z.array(z.string().min(1)).max(2).optional(),
10432
+ // The id of an existing schema this one is a CUSTOMIZATION of, when a schema
10433
+ // named `typeName` already exists in this context — required in that case,
10434
+ // and must be omitted when this create is the first schema under that name
10435
+ // (it becomes that name's shared base). Must point directly at the base (one
10436
+ // hop); immutable once set. Mirrors the `basedOn` field on the platform's
10437
+ // schema-create request.
10438
+ basedOn: z.string().min(1).optional()
10439
+ }).strict().superRefine((schema, ctx) => {
10440
+ const hasComposite = (schema.lookupFields ?? []).some(
10441
+ (lf) => typeof lf === "object" && lf.fieldNames !== void 0
10442
+ );
10443
+ if (!hasComposite) return;
10444
+ const surfaces = schema.allowedSurfaces;
10445
+ const isRecordOnly = surfaces === void 0 || surfaces.length === 1 && surfaces[0] === "record";
10446
+ if (!isRecordOnly) {
10447
+ ctx.addIssue({
10448
+ code: z.ZodIssueCode.custom,
10449
+ path: ["allowedSurfaces"],
10450
+ 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`
10451
+ });
10452
+ }
10453
+ });
10321
10454
  var BlueprintAccessProfileSchema = z.object({
10322
10455
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10323
10456
  // what enforces the data-plane-only security boundary.
@@ -12256,9 +12389,10 @@ var secondBrain = {
12256
12389
  // Lookup shapes are MIGRATION-LOCKED once the schema is live (a field cannot
12257
12390
  // flip equality↔range later), so each is chosen on purpose:
12258
12391
  // • `source` — EQUALITY, sorted by `lastUpdated`: enumerate notes from one
12259
- // source, most-recently-touched first. `lastUpdated` is always present, so
12260
- // the sort never drops a note (sorting an equality lookup by an OPTIONAL
12261
- // field would silently exclude rows that lack it).
12392
+ // source, most-recently-touched first. A platform timestamp is the natural
12393
+ // choice for "most recently touched" and is always present, so every note
12394
+ // has a position. (Sorting by an OPTIONAL payload field is also fine — notes
12395
+ // lacking it are ordered ahead of those that have it, never dropped.)
12262
12396
  // • `status` — EQUALITY: "show archived notes".
12263
12397
  // • `capturedAt` — RANGE: ordered `from`/`to`/`prefix` ("captured in 2026-06").
12264
12398
  // `externalId` has a built-in first-class finder — look it up directly, never
@@ -13380,7 +13514,16 @@ function toSdkFields(blueprint, schemaIndex) {
13380
13514
  function toLookupDefs(lookupFields) {
13381
13515
  return lookupFields?.map((lf) => {
13382
13516
  if (typeof lf === "string") return { fieldName: lf };
13383
- const def = { fieldName: lf.fieldName };
13517
+ const hasFieldName = lf.fieldName !== void 0;
13518
+ const hasFieldNames = lf.fieldNames !== void 0;
13519
+ if (hasFieldName === hasFieldNames) {
13520
+ throw new Error(
13521
+ `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.`
13522
+ );
13523
+ }
13524
+ const def = {};
13525
+ if (lf.fieldName !== void 0) def.fieldName = lf.fieldName;
13526
+ if (lf.fieldNames !== void 0) def.fieldNames = lf.fieldNames;
13384
13527
  if (lf.unique !== void 0) def.unique = lf.unique;
13385
13528
  if (lf.rangeEnabled !== void 0) def.rangeEnabled = lf.rangeEnabled;
13386
13529
  if (lf.sortBy !== void 0) def.sortBy = lf.sortBy;
@@ -13474,7 +13617,8 @@ async function runLoader(client, input) {
13474
13617
  capabilities: toCapabilities(s.capabilities),
13475
13618
  active: s.active,
13476
13619
  userId: s.userId,
13477
- scopes: s.scopes
13620
+ scopes: s.scopes,
13621
+ basedOn: s.basedOn
13478
13622
  };
13479
13623
  const resp = await ctxClient.schemas.createSchema({ upsert: true, body: schemaBody });
13480
13624
  if (resp.id) schemaIds[s.typeName] = resp.id;
@@ -13642,7 +13786,7 @@ function buildPlan(blueprint, opts = {}) {
13642
13786
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
13643
13787
  });
13644
13788
  for (const s of blueprint.schemas) {
13645
- const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldName) ?? [];
13789
+ const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
13646
13790
  const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
13647
13791
  const surfaceNote = s.allowedSurfaces ? ` surfaces: [${s.allowedSurfaces.join(", ")}]` : "";
13648
13792
  const sensitive = s.fields.filter((f) => f.sensitive).map((f) => f.fieldId);
@@ -14234,10 +14378,121 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
14234
14378
  // src/cli-blueprint.ts
14235
14379
  import { writeFile as writeFile2, access } from "fs/promises";
14236
14380
 
14381
+ // src/scope-grammar.ts
14382
+ var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
14383
+ "user",
14384
+ "record",
14385
+ "document",
14386
+ "entity",
14387
+ "self",
14388
+ "tenant",
14389
+ "context",
14390
+ "scope",
14391
+ "versions",
14392
+ "lookup"
14393
+ ]);
14394
+ var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
14395
+ var SCOPE_NAMESPACE_MIN_LENGTH = 2;
14396
+ var SCOPE_NAMESPACE_MAX_LENGTH = 32;
14397
+ var SCOPE_VALUE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
14398
+ var SCOPE_VALUE_MIN_LENGTH = 1;
14399
+ var SCOPE_VALUE_MAX_LENGTH = 128;
14400
+ function valueGrammarError(value) {
14401
+ if (!SCOPE_VALUE_RE.test(value)) {
14402
+ 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.`;
14403
+ }
14404
+ return null;
14405
+ }
14406
+ function namespaceGrammarError(ns) {
14407
+ if (FORBIDDEN_SCOPE_NAMESPACES.has(ns)) {
14408
+ return `'${ns}' is a reserved namespace and cannot be used as an ownership scope`;
14409
+ }
14410
+ if (!SCOPE_NAMESPACE_RE.test(ns)) {
14411
+ 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 '-'`;
14412
+ }
14413
+ return null;
14414
+ }
14415
+ function assertScopeEntries(entries, flag) {
14416
+ if (!entries?.length) return;
14417
+ for (const raw of entries) {
14418
+ const sep = raw.indexOf(":");
14419
+ if (sep <= 0 || sep === raw.length - 1) {
14420
+ throw new Error(`${flag} must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
14421
+ }
14422
+ const nsError = namespaceGrammarError(raw.slice(0, sep));
14423
+ if (nsError) throw new Error(`${flag} namespace ${nsError}`);
14424
+ const valError = valueGrammarError(raw.slice(sep + 1));
14425
+ if (valError) throw new Error(`${flag} value ${valError}`);
14426
+ }
14427
+ }
14428
+ function assertIdentityOverrideValues(overrides, flag) {
14429
+ if (!overrides) return;
14430
+ for (const [key, value] of Object.entries(overrides)) {
14431
+ if (!key.startsWith("scope:")) continue;
14432
+ const error = valueGrammarError(typeof value === "string" ? value : String(value));
14433
+ if (error) throw new Error(`${flag} '${key}' value ${error}`);
14434
+ }
14435
+ }
14436
+ function placeholderScopeNamespace(value) {
14437
+ const m = /^\$\{\{\s*(?:under\.)?self\.scope\.([^\s}]+)\s*\}\}$/.exec(value);
14438
+ return m ? m[1] : null;
14439
+ }
14440
+ var SELF_PLACEHOLDER_RE = /^\$\{\{\s*self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
14441
+ var UNDER_SELF_PLACEHOLDER_RE = /^\$\{\{\s*under\.self\.(?:userId|partnerUserId|scope\.[a-z][a-z0-9_-]{1,31})\s*\}\}$/;
14442
+
14237
14443
  // src/lint.ts
14238
14444
  var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
14239
- function lookupFieldName(lf) {
14240
- return typeof lf === "string" ? lf : lf.fieldName;
14445
+ var BUILT_IN_SORT_BY = /* @__PURE__ */ new Set(["createdAt", "lastUpdated"]);
14446
+ function lookupFieldNames(lf) {
14447
+ if (typeof lf === "string") return [lf];
14448
+ if (lf.fieldNames !== void 0) return lf.fieldNames;
14449
+ return lf.fieldName !== void 0 ? [lf.fieldName] : [];
14450
+ }
14451
+ function compositeLegFindings(schema, lf, fieldsById, rangeEnabledFieldNames) {
14452
+ const findings = [];
14453
+ const id = `[${lf.fieldNames.join(", ")}]`;
14454
+ const seenLegs = /* @__PURE__ */ new Set();
14455
+ for (const leg of lf.fieldNames) {
14456
+ if (seenLegs.has(leg)) {
14457
+ findings.push({
14458
+ severity: "error",
14459
+ rule: "lookup-composite-repeated-leg",
14460
+ 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.`
14461
+ });
14462
+ }
14463
+ seenLegs.add(leg);
14464
+ const field = fieldsById.get(leg);
14465
+ if (!field) continue;
14466
+ if (field.fieldType === "array" || field.fieldType === "object") {
14467
+ findings.push({
14468
+ severity: "error",
14469
+ rule: "lookup-composite-leg-multi-valued",
14470
+ 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.`
14471
+ });
14472
+ }
14473
+ if (field.fieldType === "reference" && field.cardinality === "many") {
14474
+ findings.push({
14475
+ severity: "error",
14476
+ rule: "lookup-composite-leg-multi-valued",
14477
+ 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.`
14478
+ });
14479
+ }
14480
+ if (rangeEnabledFieldNames.has(leg)) {
14481
+ findings.push({
14482
+ severity: "error",
14483
+ rule: "lookup-composite-leg-range-enabled",
14484
+ 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.`
14485
+ });
14486
+ }
14487
+ if (lf.sortBy !== void 0 && leg === lf.sortBy) {
14488
+ findings.push({
14489
+ severity: "error",
14490
+ rule: "lookup-composite-leg-is-sortby",
14491
+ 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'.`
14492
+ });
14493
+ }
14494
+ }
14495
+ return findings;
14241
14496
  }
14242
14497
  function lintBlueprint(bp) {
14243
14498
  const findings = [];
@@ -14245,6 +14500,7 @@ function lintBlueprint(bp) {
14245
14500
  for (const schema of bp.schemas) {
14246
14501
  typeNames.add(schema.typeName);
14247
14502
  const fieldIds = /* @__PURE__ */ new Set();
14503
+ const fieldsById = /* @__PURE__ */ new Map();
14248
14504
  for (const f of schema.fields) {
14249
14505
  if (fieldIds.has(f.fieldId)) {
14250
14506
  findings.push({
@@ -14254,6 +14510,7 @@ function lintBlueprint(bp) {
14254
14510
  });
14255
14511
  }
14256
14512
  fieldIds.add(f.fieldId);
14513
+ fieldsById.set(f.fieldId, f);
14257
14514
  if (f.enumValues && f.enumValues.length > 0 && !ENUM_COMPATIBLE_TYPES.has(f.fieldType)) {
14258
14515
  findings.push({
14259
14516
  severity: "warning",
@@ -14262,14 +14519,35 @@ function lintBlueprint(bp) {
14262
14519
  });
14263
14520
  }
14264
14521
  }
14522
+ const rangeEnabledFieldNames = /* @__PURE__ */ new Set();
14265
14523
  for (const lf of schema.lookupFields ?? []) {
14266
- const name = lookupFieldName(lf);
14267
- if (!fieldIds.has(name)) {
14268
- findings.push({
14269
- severity: "error",
14270
- rule: "lookup-field-unknown",
14271
- message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
14272
- });
14524
+ if (typeof lf !== "string" && lf.fieldName !== void 0 && lf.rangeEnabled) {
14525
+ rangeEnabledFieldNames.add(lf.fieldName);
14526
+ }
14527
+ }
14528
+ for (const lf of schema.lookupFields ?? []) {
14529
+ for (const name of lookupFieldNames(lf)) {
14530
+ if (!fieldIds.has(name)) {
14531
+ findings.push({
14532
+ severity: "error",
14533
+ rule: "lookup-field-unknown",
14534
+ message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
14535
+ });
14536
+ }
14537
+ }
14538
+ if (typeof lf !== "string" && lf.fieldNames !== void 0) {
14539
+ findings.push(
14540
+ ...compositeLegFindings(schema, { fieldNames: lf.fieldNames, sortBy: lf.sortBy }, fieldsById, rangeEnabledFieldNames)
14541
+ );
14542
+ }
14543
+ if (typeof lf !== "string" && lf.sortBy !== void 0) {
14544
+ if (!BUILT_IN_SORT_BY.has(lf.sortBy) && !fieldIds.has(lf.sortBy)) {
14545
+ findings.push({
14546
+ severity: "warning",
14547
+ rule: "lookup-sortby-unknown-field",
14548
+ 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.`
14549
+ });
14550
+ }
14273
14551
  }
14274
14552
  }
14275
14553
  }
@@ -14285,6 +14563,10 @@ function lintBlueprint(bp) {
14285
14563
  const ds = bp.accessProfile.dataScope;
14286
14564
  if (ds) {
14287
14565
  for (const [key, values] of Object.entries(ds)) {
14566
+ for (const value of values) {
14567
+ const bad = placeholderFinding("accessProfile", key, value);
14568
+ if (bad) findings.push(bad);
14569
+ }
14288
14570
  if (values.length > 0 && !values.includes(null)) {
14289
14571
  findings.push({
14290
14572
  severity: "warning",
@@ -14308,6 +14590,10 @@ function lintBlueprint(bp) {
14308
14590
  const cds = clause.dataScope;
14309
14591
  if (!cds) return;
14310
14592
  for (const [key, values] of Object.entries(cds)) {
14593
+ for (const value of values) {
14594
+ const bad = placeholderFinding(`role '${roleId}' clause ${i}`, key, value);
14595
+ if (bad) findings.push(bad);
14596
+ }
14311
14597
  if (values.length > 0 && !values.includes(null)) {
14312
14598
  findings.push({
14313
14599
  severity: "warning",
@@ -14321,6 +14607,28 @@ function lintBlueprint(bp) {
14321
14607
  return findings;
14322
14608
  }
14323
14609
  var ROLE_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
14610
+ var PLACEHOLDER_SHAPED_RE = /^\s*\$\{\{.*\}\}\s*$/s;
14611
+ var RECOGNIZED_PLACEHOLDER_RES = [/^\$\{\{\s*any\s*\}\}$/, SELF_PLACEHOLDER_RE, UNDER_SELF_PLACEHOLDER_RE];
14612
+ var BLUEPRINT_TOKEN_RE = /^\s*\$\{\{\s*(identities|inputs|vectros|ns)\./;
14613
+ function placeholderFinding(where, key, value) {
14614
+ if (typeof value !== "string" || !PLACEHOLDER_SHAPED_RE.test(value)) return null;
14615
+ if (BLUEPRINT_TOKEN_RE.test(value)) return null;
14616
+ if (RECOGNIZED_PLACEHOLDER_RES.some((re) => re.test(value))) {
14617
+ const ns = placeholderScopeNamespace(value);
14618
+ const nsError = ns === null ? null : namespaceGrammarError(ns);
14619
+ if (nsError === null) return null;
14620
+ return {
14621
+ severity: "error",
14622
+ rule: "datascope-unknown-placeholder",
14623
+ message: `${where}: dataScope.${key} value '${value}' names a namespace the platform rejects \u2014 ${nsError}. It will be rejected at apply.`
14624
+ };
14625
+ }
14626
+ return {
14627
+ severity: "error",
14628
+ rule: "datascope-unknown-placeholder",
14629
+ 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.`
14630
+ };
14631
+ }
14324
14632
  function lintPasses(findings) {
14325
14633
  return !findings.some((f) => f.severity === "error");
14326
14634
  }
@@ -14724,11 +15032,14 @@ function parseScopeOverrides(scopes) {
14724
15032
  throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
14725
15033
  }
14726
15034
  const ns = raw.slice(0, sep);
15035
+ const value = raw.slice(sep + 1);
15036
+ const valueError = valueGrammarError(value);
15037
+ if (valueError) throw new Error(`--scope value ${valueError}`);
14727
15038
  const key = `scope:${ns}`;
14728
15039
  if (Object.prototype.hasOwnProperty.call(out, key)) {
14729
15040
  throw new Error(`--scope sets namespace '${ns}' more than once \u2014 a scope override takes at most one value per namespace.`);
14730
15041
  }
14731
- out[key] = raw.slice(sep + 1);
15042
+ out[key] = value;
14732
15043
  }
14733
15044
  if (Object.keys(out).length > 2) {
14734
15045
  throw new Error("--scope allows at most two distinct namespaces.");
@@ -15073,6 +15384,7 @@ async function createIdentity(client, opts, payload) {
15073
15384
  }
15074
15385
  });
15075
15386
  }
15387
+ assertScopeEntries(opts.scope, "--scope");
15076
15388
  return client.identity.createEntity({
15077
15389
  namespace: opts.type,
15078
15390
  body: {
@@ -15278,6 +15590,7 @@ async function runAccessGrant(opts, deps = {}) {
15278
15590
  ...parseScopeOverrides(opts.scope) ?? {},
15279
15591
  ...opts.identityOverrides ? parseJsonObjectFlag("--identity-overrides", opts.identityOverrides) : {}
15280
15592
  };
15593
+ assertIdentityOverrideValues(identityOverrides, "--identity-overrides");
15281
15594
  } catch (err) {
15282
15595
  process.stderr.write(` \u2716 ${formatError(err)}
15283
15596
  `);
@@ -15430,30 +15743,21 @@ ${renderProfile(p)}
15430
15743
 
15431
15744
  // src/commands/access-explain.ts
15432
15745
  var SELF = /* @__PURE__ */ Symbol("self");
15746
+ var ANY = /* @__PURE__ */ Symbol("any");
15747
+ var UNDER_SELF = /* @__PURE__ */ Symbol("under.self");
15433
15748
  function canonicalDim(key) {
15434
15749
  if (key === "userId" || key === "partnerUserId") return "user";
15435
15750
  return key;
15436
15751
  }
15437
- var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
15438
- "user",
15439
- "record",
15440
- "document",
15441
- "entity",
15442
- "self",
15443
- "tenant",
15444
- "context",
15445
- "scope",
15446
- "versions",
15447
- "lookup"
15448
- ]);
15449
- var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
15450
15752
  function isRecognizedOwnershipKey(key) {
15753
+ if (key === DIM_WILDCARD) return true;
15451
15754
  if (key === "userId" || key === "partnerUserId") return true;
15452
15755
  if (!key.startsWith("scope:")) return false;
15453
15756
  const ns = key.slice("scope:".length);
15454
15757
  return !FORBIDDEN_SCOPE_NAMESPACES.has(ns) && SCOPE_NAMESPACE_RE.test(ns);
15455
15758
  }
15456
- var SELF_TOKEN_RE2 = /^\$\{\{\s*self\.[a-zA-Z.]+\s*\}\}$/;
15759
+ var DIM_WILDCARD = "*";
15760
+ var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
15457
15761
  function normalizeClauses(raw) {
15458
15762
  const out = [];
15459
15763
  for (const c of raw) {
@@ -15469,7 +15773,9 @@ function normalizeClauses(raw) {
15469
15773
  dataScope[dim] = values.map((val) => {
15470
15774
  if (val === null) return null;
15471
15775
  const s = String(val);
15472
- return SELF_TOKEN_RE2.test(s) ? SELF : s;
15776
+ if (ANY_TOKEN_RE.test(s)) return ANY;
15777
+ if (UNDER_SELF_PLACEHOLDER_RE.test(s)) return UNDER_SELF;
15778
+ return SELF_PLACEHOLDER_RE.test(s) ? SELF : s;
15473
15779
  });
15474
15780
  if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
15475
15781
  }
@@ -15477,6 +15783,38 @@ function normalizeClauses(raw) {
15477
15783
  }
15478
15784
  return out;
15479
15785
  }
15786
+ function recordTypesOf(actions) {
15787
+ if (actions.includes("*")) return "all";
15788
+ const types = /* @__PURE__ */ new Set();
15789
+ let sawRecordsAction = false;
15790
+ let sawUntyped = false;
15791
+ for (const action of actions) {
15792
+ const parts = action.split(":");
15793
+ if (parts[0] !== "records") continue;
15794
+ if (!(parts[1] ?? "").includes("r")) continue;
15795
+ if (parts.length >= 3) {
15796
+ const type = parts.slice(2).join(":");
15797
+ if (type === "") continue;
15798
+ if (type === "*") continue;
15799
+ if (type.endsWith(":*")) {
15800
+ const stripped = type.slice(0, -2);
15801
+ if (stripped === "" || stripped.includes(":")) continue;
15802
+ sawRecordsAction = true;
15803
+ types.add(stripped);
15804
+ continue;
15805
+ }
15806
+ if (type.includes(":")) continue;
15807
+ sawRecordsAction = true;
15808
+ types.add(type);
15809
+ } else {
15810
+ sawRecordsAction = true;
15811
+ sawUntyped = true;
15812
+ }
15813
+ }
15814
+ if (!sawRecordsAction) return null;
15815
+ if (sawUntyped) return "all";
15816
+ return [...types].sort();
15817
+ }
15480
15818
  function buildArchetypes(clauses) {
15481
15819
  const rows = [
15482
15820
  { label: "a record you own alone (private)", owners: { user: SELF } },
@@ -15487,8 +15825,13 @@ function buildArchetypes(clauses) {
15487
15825
  if (c.unrecognizedDims.length > 0) continue;
15488
15826
  for (const [dim, values] of Object.entries(c.dataScope)) {
15489
15827
  if (dim === "user") continue;
15828
+ if (dim === DIM_WILDCARD) continue;
15490
15829
  const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
15491
- for (const v of values) if (v !== null) set.add(v);
15830
+ set.add(SELF);
15831
+ for (const v of values) {
15832
+ if (v === null || v === ANY || v === UNDER_SELF) continue;
15833
+ set.add(v);
15834
+ }
15492
15835
  scopeDims.set(dim, set);
15493
15836
  }
15494
15837
  }
@@ -15507,19 +15850,42 @@ function buildArchetypes(clauses) {
15507
15850
  rows.push({ label: "no owner values at all (tenant-shared)", owners: {} });
15508
15851
  return rows;
15509
15852
  }
15853
+ function listAdmits(allowed, recordValue) {
15854
+ return allowed.some((a) => {
15855
+ if (a === null) return recordValue === void 0;
15856
+ if (a === SELF) return recordValue === SELF;
15857
+ if (a === ANY) return recordValue !== void 0;
15858
+ if (a === UNDER_SELF) return false;
15859
+ return recordValue === a;
15860
+ });
15861
+ }
15510
15862
  function clauseMatches(clause, arch) {
15511
15863
  if (clause.unrecognizedDims.length > 0) return false;
15864
+ const wildcard = clause.dataScope[DIM_WILDCARD];
15512
15865
  for (const [dim, allowed] of Object.entries(clause.dataScope)) {
15513
- const recordValue = arch.owners[dim];
15514
- const ok = allowed.some((a) => {
15515
- if (a === null) return recordValue === void 0;
15516
- if (a === SELF) return recordValue === SELF;
15517
- return recordValue === a;
15518
- });
15519
- if (!ok) return false;
15866
+ if (dim === DIM_WILDCARD) continue;
15867
+ if (!listAdmits(allowed, arch.owners[dim])) return false;
15868
+ }
15869
+ if (wildcard) {
15870
+ let carriesAny = false;
15871
+ for (const [dim, value] of Object.entries(arch.owners)) {
15872
+ carriesAny = true;
15873
+ if (dim in clause.dataScope) continue;
15874
+ if (!listAdmits(wildcard, value)) return false;
15875
+ }
15876
+ if (!carriesAny && !listAdmits(wildcard, void 0)) return false;
15520
15877
  }
15521
15878
  return true;
15522
15879
  }
15880
+ function undecidableDims(clauses) {
15881
+ const dims = /* @__PURE__ */ new Set();
15882
+ for (const c of clauses) {
15883
+ for (const [dim, values] of Object.entries(c.dataScope)) {
15884
+ if (values.includes(UNDER_SELF)) dims.add(dim);
15885
+ }
15886
+ }
15887
+ return [...dims];
15888
+ }
15523
15889
  function explainClauses(clauses) {
15524
15890
  const archetypes = buildArchetypes(clauses);
15525
15891
  return archetypes.map((arch) => {
@@ -15531,15 +15897,34 @@ function explainClauses(clauses) {
15531
15897
  for (const a of c.actions) actions.add(a);
15532
15898
  }
15533
15899
  });
15534
- return { archetype: arch.label, clauses: matched, actions: [...actions] };
15900
+ const recordTypes = recordTypesOf([...actions]);
15901
+ return {
15902
+ archetype: arch.label,
15903
+ clauses: matched,
15904
+ actions: [...actions],
15905
+ ...recordTypes !== null ? { recordTypes } : {}
15906
+ };
15535
15907
  });
15536
15908
  }
15537
15909
  function renderDataScope(ds) {
15538
15910
  const entries = Object.entries(ds);
15539
15911
  if (entries.length === 0) return "(unscoped \u2014 all data in the context)";
15540
- return entries.map(
15541
- ([dim, values]) => `${dim}:[${values.map((v) => v === SELF ? "self" : v === null ? "null" : v).join(", ")}]`
15542
- ).join(" AND ");
15912
+ return entries.map(([dim, values]) => {
15913
+ const shown = values.map((v) => {
15914
+ if (v === SELF) return "self";
15915
+ if (v === ANY) return "any";
15916
+ if (v === UNDER_SELF) return "under-self";
15917
+ if (v === null) return "null";
15918
+ return v;
15919
+ });
15920
+ const label = dim === DIM_WILDCARD ? "* (every other dimension)" : dim;
15921
+ return `${label}:[${shown.join(", ")}]`;
15922
+ }).join(" AND ");
15923
+ }
15924
+ function renderRecordTypes(recordTypes) {
15925
+ if (recordTypes === void 0) return "\u2014";
15926
+ if (recordTypes === "all") return "all types";
15927
+ return recordTypes.join(", ");
15543
15928
  }
15544
15929
  var SELF_PRINCIPAL_ALIAS2 = "me";
15545
15930
  async function runAccessExplain(opts, deps = {}) {
@@ -15567,6 +15952,7 @@ async function runAccessExplain(opts, deps = {}) {
15567
15952
  }
15568
15953
  const clauses = normalizeClauses(rawClauses);
15569
15954
  const rows = explainClauses(clauses);
15955
+ const undecidable = undecidableDims(clauses);
15570
15956
  if (opts.json) {
15571
15957
  process.stdout.write(
15572
15958
  `${formatJson({
@@ -15583,7 +15969,10 @@ async function runAccessExplain(opts, deps = {}) {
15583
15969
  ...c.unrecognizedDims.length > 0 ? { grantsNothing: true, unrecognizedDims: c.unrecognizedDims } : {}
15584
15970
  })),
15585
15971
  matrix: rows,
15586
- 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."
15972
+ // Present only when a clause uses `${{ under.self.* }}`: those rows are UNDECIDED
15973
+ // offline, not denied, and a consumer reading a blank cell as a denial would be wrong.
15974
+ ...undecidable.length > 0 ? { undecidableDims: undecidable } : {},
15975
+ 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."
15587
15976
  })}
15588
15977
  `
15589
15978
  );
@@ -15611,8 +16000,19 @@ async function runAccessExplain(opts, deps = {}) {
15611
16000
  "\n" + formatTable(rows, [
15612
16001
  { header: "record owned by\u2026", get: (r) => r.archetype },
15613
16002
  { header: "granted?", get: (r) => r.clauses.length ? `via clause ${r.clauses.join(", ")}` : "NO" },
15614
- { header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" }
15615
- ]) + "\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"
16003
+ { header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" },
16004
+ // The ownership "granted?" cell says nothing about WHICH record types — a clause can be
16005
+ // ownership-unscoped and still type-restricted (a curated shared-KB clause with no owner
16006
+ // dimension, readable only for a few named types, never others). Surface the actual
16007
+ // `records:` reach per row instead of leaving it to a footnote.
16008
+ { header: "record types", get: (r) => renderRecordTypes(r.recordTypes) }
16009
+ ]) + (undecidable.length > 0 ? `
16010
+
16011
+ \u26A0 UNDECIDED offline: ${undecidable.join(", ")}
16012
+ A clause matching on \`\${{ under.self.* }}\` needs the parent graph, which only the server has,
16013
+ so rows granted through it are shown as NO above. Read those cells as "cannot tell here",
16014
+ NOT as a denial \u2014 the matrix under-reports this clause.
16015
+ ` : "") + '\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'
15616
16016
  );
15617
16017
  return 0;
15618
16018
  } catch (err) {
@@ -18351,9 +18751,9 @@ function emitJoined(alias, context, principalId, role, keyId, reused) {
18351
18751
 
18352
18752
  // src/build-info.ts
18353
18753
  var BUILD_INFO = {
18354
- cli: true ? "0.11.0" : "dev",
18355
- sdk: true ? "0.36.0" : "dev",
18356
- blueprints: true ? "0.8.0" : "dev"
18754
+ cli: true ? "0.13.0" : "dev",
18755
+ sdk: true ? "0.38.0" : "dev",
18756
+ blueprints: true ? "0.10.0" : "dev"
18357
18757
  };
18358
18758
  function formatBuildInfo() {
18359
18759
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;