@vectros-ai/cli 0.12.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
@@ -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) {
@@ -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:
@@ -6563,7 +6566,7 @@ var DocumentsClient = class {
6563
6566
  * field: "po_number",
6564
6567
  * value: "PO-1001",
6565
6568
  * prefix: "PO-2024",
6566
- * startFrom: "550e8400-e29b-41d4-a716-446655440000",
6569
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
6567
6570
  * userId: "550e8400-e29b-41d4-a716-446655440000",
6568
6571
  * scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
6569
6572
  * })
@@ -6939,7 +6942,7 @@ var FoldersClient = class {
6939
6942
  * parentFolderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
6940
6943
  * userId: "550e8400-e29b-41d4-a716-446655440000",
6941
6944
  * scope: "group:eng-team",
6942
- * startFrom: "fld_prev123"
6945
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
6943
6946
  * })
6944
6947
  */
6945
6948
  listFolders(request = {}, requestOptions) {
@@ -7116,6 +7119,7 @@ var FoldersClient = class {
7116
7119
  * @param {Vectros.UpdateFolderRequest} request
7117
7120
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
7118
7121
  *
7122
+ * @throws {@link Vectros.ForbiddenError}
7119
7123
  * @throws {@link Vectros.NotFoundError}
7120
7124
  * @throws {@link Vectros.TooManyRequestsError}
7121
7125
  *
@@ -7160,6 +7164,8 @@ var FoldersClient = class {
7160
7164
  }
7161
7165
  if (_response.error.reason === "status-code") {
7162
7166
  switch (_response.error.statusCode) {
7167
+ case 403:
7168
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
7163
7169
  case 404:
7164
7170
  throw new NotFoundError(_response.error.body, _response.rawResponse);
7165
7171
  case 429:
@@ -7242,6 +7248,7 @@ var FoldersClient = class {
7242
7248
  * @param {FoldersClient.RequestOptions} requestOptions - Request-specific configuration.
7243
7249
  *
7244
7250
  * @throws {@link Vectros.BadRequestError}
7251
+ * @throws {@link Vectros.ForbiddenError}
7245
7252
  * @throws {@link Vectros.NotFoundError}
7246
7253
  * @throws {@link Vectros.ConflictError}
7247
7254
  * @throws {@link Vectros.TooManyRequestsError}
@@ -7289,6 +7296,8 @@ var FoldersClient = class {
7289
7296
  switch (_response.error.statusCode) {
7290
7297
  case 400:
7291
7298
  throw new BadRequestError(_response.error.body, _response.rawResponse);
7299
+ case 403:
7300
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
7292
7301
  case 404:
7293
7302
  throw new NotFoundError(_response.error.body, _response.rawResponse);
7294
7303
  case 409:
@@ -7376,7 +7385,8 @@ var IdentityClient = class {
7376
7385
  *
7377
7386
  * @example
7378
7387
  * await client.identity.listEntities({
7379
- * namespace: "team"
7388
+ * namespace: "team",
7389
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
7380
7390
  * })
7381
7391
  */
7382
7392
  listEntities(request, requestOptions) {
@@ -8140,7 +8150,7 @@ var IdentityClient = class {
8140
8150
  * @example
8141
8151
  * await client.identity.listUsers({
8142
8152
  * externalId: "usr_12345",
8143
- * startFrom: "550e8400-e29b-41d4-a716-446655440000",
8153
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl",
8144
8154
  * type: "person_v1",
8145
8155
  * field: "team",
8146
8156
  * value: "engineering",
@@ -8321,11 +8331,12 @@ var IdentityClient = class {
8321
8331
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/users/{id}");
8322
8332
  }
8323
8333
  /**
8324
- * 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.
8325
8335
  *
8326
8336
  * @param {Vectros.UpdateUserRequest} request
8327
8337
  * @param {IdentityClient.RequestOptions} requestOptions - Request-specific configuration.
8328
8338
  *
8339
+ * @throws {@link Vectros.BadRequestError}
8329
8340
  * @throws {@link Vectros.NotFoundError}
8330
8341
  * @throws {@link Vectros.TooManyRequestsError}
8331
8342
  *
@@ -8370,6 +8381,8 @@ var IdentityClient = class {
8370
8381
  }
8371
8382
  if (_response.error.reason === "status-code") {
8372
8383
  switch (_response.error.statusCode) {
8384
+ case 400:
8385
+ throw new BadRequestError(_response.error.body, _response.rawResponse);
8373
8386
  case 404:
8374
8387
  throw new NotFoundError(_response.error.body, _response.rawResponse);
8375
8388
  case 429:
@@ -9017,7 +9030,7 @@ var RecordsClient = class {
9017
9030
  * folderId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
9018
9031
  * userId: "550e8400-e29b-41d4-a716-446655440000",
9019
9032
  * scope: "group:eng-team",
9020
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
9033
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9021
9034
  * })
9022
9035
  */
9023
9036
  listRecords(request = {}, requestOptions) {
@@ -9205,6 +9218,7 @@ var RecordsClient = class {
9205
9218
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9206
9219
  *
9207
9220
  * @throws {@link Vectros.BadRequestError}
9221
+ * @throws {@link Vectros.ForbiddenError}
9208
9222
  * @throws {@link Vectros.NotFoundError}
9209
9223
  * @throws {@link Vectros.ConflictError}
9210
9224
  * @throws {@link Vectros.TooManyRequestsError}
@@ -9253,6 +9267,8 @@ var RecordsClient = class {
9253
9267
  switch (_response.error.statusCode) {
9254
9268
  case 400:
9255
9269
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9270
+ case 403:
9271
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9256
9272
  case 404:
9257
9273
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9258
9274
  case 409:
@@ -9334,6 +9350,7 @@ var RecordsClient = class {
9334
9350
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
9335
9351
  *
9336
9352
  * @throws {@link Vectros.BadRequestError}
9353
+ * @throws {@link Vectros.ForbiddenError}
9337
9354
  * @throws {@link Vectros.NotFoundError}
9338
9355
  * @throws {@link Vectros.ConflictError}
9339
9356
  * @throws {@link Vectros.TooManyRequestsError}
@@ -9384,6 +9401,8 @@ var RecordsClient = class {
9384
9401
  switch (_response.error.statusCode) {
9385
9402
  case 400:
9386
9403
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9404
+ case 403:
9405
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9387
9406
  case 404:
9388
9407
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9389
9408
  case 409:
@@ -9401,7 +9420,15 @@ var RecordsClient = class {
9401
9420
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "PATCH", "/v1/records/{id}");
9402
9421
  }
9403
9422
  /**
9404
- * 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.
9405
9432
  *
9406
9433
  * @param {Vectros.LookupRecordsRequest} request
9407
9434
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9413,22 +9440,40 @@ var RecordsClient = class {
9413
9440
  * type: "intake_form",
9414
9441
  * field: "email",
9415
9442
  * value: "jane@example.com",
9443
+ * values: ["open"],
9416
9444
  * prefix: "jane",
9417
- * startFrom: "550e8400-e29b-41d4-a716-446655440000"
9445
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9418
9446
  * })
9419
9447
  */
9420
9448
  lookupRecords(request, requestOptions) {
9421
9449
  return HttpResponsePromise.fromPromise(this.__lookupRecords(request, requestOptions));
9422
9450
  }
9423
9451
  async __lookupRecords(request, requestOptions) {
9424
- 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;
9425
9467
  const _queryParams = {
9426
9468
  type: type_,
9427
9469
  field,
9428
9470
  value,
9471
+ values,
9429
9472
  from: from_,
9430
9473
  to,
9431
9474
  prefix,
9475
+ sortFrom,
9476
+ sortTo,
9432
9477
  startFrom,
9433
9478
  limit,
9434
9479
  includePayload,
@@ -9472,7 +9517,7 @@ var RecordsClient = class {
9472
9517
  return handleNonStatusCodeError(_response.error, _response.rawResponse, "GET", "/v1/records/lookup");
9473
9518
  }
9474
9519
  /**
9475
- * 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.
9476
9521
  *
9477
9522
  * @param {Vectros.RecordLookupRequest} request
9478
9523
  * @param {RecordsClient.RequestOptions} requestOptions - Request-specific configuration.
@@ -9667,7 +9712,7 @@ var SchemasClient = class {
9667
9712
  * scope: "org:6ba7b810-9dad-11d1-80b4-00c04fd430c8",
9668
9713
  * surface: "document",
9669
9714
  * recordType: "intake_form",
9670
- * startFrom: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
9715
+ * startFrom: "b3BhcXVlLWN1cnNvci1mcm9tLXRoZS1wcmV2aW91cy1wYWdl"
9671
9716
  * })
9672
9717
  */
9673
9718
  listSchemas(request = {}, requestOptions) {
@@ -9871,6 +9916,7 @@ var SchemasClient = class {
9871
9916
  * @param {SchemasClient.RequestOptions} requestOptions - Request-specific configuration.
9872
9917
  *
9873
9918
  * @throws {@link Vectros.BadRequestError}
9919
+ * @throws {@link Vectros.ForbiddenError}
9874
9920
  * @throws {@link Vectros.NotFoundError}
9875
9921
  * @throws {@link Vectros.TooManyRequestsError}
9876
9922
  *
@@ -9919,6 +9965,8 @@ var SchemasClient = class {
9919
9965
  switch (_response.error.statusCode) {
9920
9966
  case 400:
9921
9967
  throw new BadRequestError(_response.error.body, _response.rawResponse);
9968
+ case 403:
9969
+ throw new ForbiddenError(_response.error.body, _response.rawResponse);
9922
9970
  case 404:
9923
9971
  throw new NotFoundError(_response.error.body, _response.rawResponse);
9924
9972
  case 429:
@@ -10185,7 +10233,7 @@ var logging;
10185
10233
  logging2.ConsoleLogger = ConsoleLogger;
10186
10234
  })(logging || (logging = {}));
10187
10235
 
10188
- // ../blueprints/dist/index.mjs
10236
+ // node_modules/@vectros-ai/blueprints/dist/index.mjs
10189
10237
  var import_zod = require("zod");
10190
10238
  var import_zod2 = require("zod");
10191
10239
  var CONTEXT_ID_RE = /^[a-z][a-z0-9-]{2,30}$/;
@@ -10289,19 +10337,63 @@ var BlueprintFieldDefSchema = import_zod.z.object({
10289
10337
  var BlueprintLookupFieldSchema = import_zod.z.union([
10290
10338
  import_zod.z.string().min(1),
10291
10339
  import_zod.z.object({
10292
- 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.
10293
10355
  unique: import_zod.z.boolean().optional(),
10294
10356
  // Opt this field into ordered range + prefix lookups (from/to/prefix) on
10295
10357
  // top of exact match. Billed at the range-index rate; not valid on a
10296
- // 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.
10297
10361
  rangeEnabled: import_zod.z.boolean().optional(),
10298
10362
  // Sort key for the exact-match index: 'createdAt' (default), 'lastUpdated',
10299
- // 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.
10300
10366
  sortBy: import_zod.z.string().min(1).optional(),
10301
10367
  // Opt a field past the fixed fast-index budget into a higher-cost
10302
10368
  // secondary index. No effect on a field that fits within the budget.
10303
10369
  allowOverflow: import_zod.z.boolean().optional()
10304
- }).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
+ })
10305
10397
  ]);
10306
10398
  var BlueprintCapabilitiesSchema = import_zod.z.object({
10307
10399
  auditHistory: import_zod.z.boolean().optional()
@@ -10339,7 +10431,21 @@ var BlueprintSchemaSchema = import_zod.z.object({
10339
10431
  // hop); immutable once set. Mirrors the `basedOn` field on the platform's
10340
10432
  // schema-create request.
10341
10433
  basedOn: import_zod.z.string().min(1).optional()
10342
- }).strict();
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
+ });
10343
10449
  var BlueprintAccessProfileSchema = import_zod.z.object({
10344
10450
  // Validated structurally here; the SCOPE GATE (in @vectros-ai/cli) is
10345
10451
  // what enforces the data-plane-only security boundary.
@@ -12278,9 +12384,10 @@ var secondBrain = {
12278
12384
  // Lookup shapes are MIGRATION-LOCKED once the schema is live (a field cannot
12279
12385
  // flip equality↔range later), so each is chosen on purpose:
12280
12386
  // • `source` — EQUALITY, sorted by `lastUpdated`: enumerate notes from one
12281
- // source, most-recently-touched first. `lastUpdated` is always present, so
12282
- // the sort never drops a note (sorting an equality lookup by an OPTIONAL
12283
- // 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.)
12284
12391
  // • `status` — EQUALITY: "show archived notes".
12285
12392
  // • `capturedAt` — RANGE: ordered `from`/`to`/`prefix` ("captured in 2026-06").
12286
12393
  // `externalId` has a built-in first-class finder — look it up directly, never
@@ -13402,7 +13509,16 @@ function toSdkFields(blueprint, schemaIndex) {
13402
13509
  function toLookupDefs(lookupFields) {
13403
13510
  return lookupFields?.map((lf) => {
13404
13511
  if (typeof lf === "string") return { fieldName: lf };
13405
- 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;
13406
13522
  if (lf.unique !== void 0) def.unique = lf.unique;
13407
13523
  if (lf.rangeEnabled !== void 0) def.rangeEnabled = lf.rangeEnabled;
13408
13524
  if (lf.sortBy !== void 0) def.sortBy = lf.sortBy;
@@ -13665,7 +13781,7 @@ function buildPlan(blueprint, opts = {}) {
13665
13781
  summary: `principal '${blueprint.servicePrincipal.externalId}' (SERVICE)`
13666
13782
  });
13667
13783
  for (const s of blueprint.schemas) {
13668
- const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldName) ?? [];
13784
+ const lookups = toLookupDefs(s.lookupFields)?.map((l) => l.fieldNames ? `(${l.fieldNames.join("+")})` : l.fieldName) ?? [];
13669
13785
  const lookupNote = lookups.length ? ` lookup: [${lookups.join(", ")}]` : "";
13670
13786
  const surfaceNote = s.allowedSurfaces ? ` surfaces: [${s.allowedSurfaces.join(", ")}]` : "";
13671
13787
  const sensitive = s.fields.filter((f) => f.sensitive).map((f) => f.fieldId);
@@ -14257,10 +14373,121 @@ ${formatCliError(err, { action: "Bootstrap failed" })}`);
14257
14373
  // src/cli-blueprint.ts
14258
14374
  var import_promises3 = require("fs/promises");
14259
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
+
14260
14438
  // src/lint.ts
14261
14439
  var ENUM_COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["enum", "string", "string[]"]);
14262
- function lookupFieldName(lf) {
14263
- 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;
14264
14491
  }
14265
14492
  function lintBlueprint(bp) {
14266
14493
  const findings = [];
@@ -14268,6 +14495,7 @@ function lintBlueprint(bp) {
14268
14495
  for (const schema of bp.schemas) {
14269
14496
  typeNames.add(schema.typeName);
14270
14497
  const fieldIds = /* @__PURE__ */ new Set();
14498
+ const fieldsById = /* @__PURE__ */ new Map();
14271
14499
  for (const f of schema.fields) {
14272
14500
  if (fieldIds.has(f.fieldId)) {
14273
14501
  findings.push({
@@ -14277,6 +14505,7 @@ function lintBlueprint(bp) {
14277
14505
  });
14278
14506
  }
14279
14507
  fieldIds.add(f.fieldId);
14508
+ fieldsById.set(f.fieldId, f);
14280
14509
  if (f.enumValues && f.enumValues.length > 0 && !ENUM_COMPATIBLE_TYPES.has(f.fieldType)) {
14281
14510
  findings.push({
14282
14511
  severity: "warning",
@@ -14285,14 +14514,35 @@ function lintBlueprint(bp) {
14285
14514
  });
14286
14515
  }
14287
14516
  }
14517
+ const rangeEnabledFieldNames = /* @__PURE__ */ new Set();
14288
14518
  for (const lf of schema.lookupFields ?? []) {
14289
- const name = lookupFieldName(lf);
14290
- if (!fieldIds.has(name)) {
14291
- findings.push({
14292
- severity: "error",
14293
- rule: "lookup-field-unknown",
14294
- message: `schema '${schema.typeName}': lookupField '${name}' is not a declared field.`
14295
- });
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
+ }
14296
14546
  }
14297
14547
  }
14298
14548
  }
@@ -14308,6 +14558,10 @@ function lintBlueprint(bp) {
14308
14558
  const ds = bp.accessProfile.dataScope;
14309
14559
  if (ds) {
14310
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
+ }
14311
14565
  if (values.length > 0 && !values.includes(null)) {
14312
14566
  findings.push({
14313
14567
  severity: "warning",
@@ -14331,6 +14585,10 @@ function lintBlueprint(bp) {
14331
14585
  const cds = clause.dataScope;
14332
14586
  if (!cds) return;
14333
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
+ }
14334
14592
  if (values.length > 0 && !values.includes(null)) {
14335
14593
  findings.push({
14336
14594
  severity: "warning",
@@ -14344,6 +14602,28 @@ function lintBlueprint(bp) {
14344
14602
  return findings;
14345
14603
  }
14346
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
+ }
14347
14627
  function lintPasses(findings) {
14348
14628
  return !findings.some((f) => f.severity === "error");
14349
14629
  }
@@ -14747,11 +15027,14 @@ function parseScopeOverrides(scopes) {
14747
15027
  throw new Error(`--scope must be '<namespace>:<value>' (e.g. 'org:6ba7...'); got '${raw}'.`);
14748
15028
  }
14749
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}`);
14750
15033
  const key = `scope:${ns}`;
14751
15034
  if (Object.prototype.hasOwnProperty.call(out, key)) {
14752
15035
  throw new Error(`--scope sets namespace '${ns}' more than once \u2014 a scope override takes at most one value per namespace.`);
14753
15036
  }
14754
- out[key] = raw.slice(sep + 1);
15037
+ out[key] = value;
14755
15038
  }
14756
15039
  if (Object.keys(out).length > 2) {
14757
15040
  throw new Error("--scope allows at most two distinct namespaces.");
@@ -15096,6 +15379,7 @@ async function createIdentity(client, opts, payload) {
15096
15379
  }
15097
15380
  });
15098
15381
  }
15382
+ assertScopeEntries(opts.scope, "--scope");
15099
15383
  return client.identity.createEntity({
15100
15384
  namespace: opts.type,
15101
15385
  body: {
@@ -15301,6 +15585,7 @@ async function runAccessGrant(opts, deps = {}) {
15301
15585
  ...parseScopeOverrides(opts.scope) ?? {},
15302
15586
  ...opts.identityOverrides ? parseJsonObjectFlag("--identity-overrides", opts.identityOverrides) : {}
15303
15587
  };
15588
+ assertIdentityOverrideValues(identityOverrides, "--identity-overrides");
15304
15589
  } catch (err) {
15305
15590
  process.stderr.write(` \u2716 ${formatError(err)}
15306
15591
  `);
@@ -15453,30 +15738,21 @@ ${renderProfile(p)}
15453
15738
 
15454
15739
  // src/commands/access-explain.ts
15455
15740
  var SELF = /* @__PURE__ */ Symbol("self");
15741
+ var ANY = /* @__PURE__ */ Symbol("any");
15742
+ var UNDER_SELF = /* @__PURE__ */ Symbol("under.self");
15456
15743
  function canonicalDim(key) {
15457
15744
  if (key === "userId" || key === "partnerUserId") return "user";
15458
15745
  return key;
15459
15746
  }
15460
- var FORBIDDEN_SCOPE_NAMESPACES = /* @__PURE__ */ new Set([
15461
- "user",
15462
- "record",
15463
- "document",
15464
- "entity",
15465
- "self",
15466
- "tenant",
15467
- "context",
15468
- "scope",
15469
- "versions",
15470
- "lookup"
15471
- ]);
15472
- var SCOPE_NAMESPACE_RE = /^[a-z][a-z0-9_-]{1,31}$/;
15473
15747
  function isRecognizedOwnershipKey(key) {
15748
+ if (key === DIM_WILDCARD) return true;
15474
15749
  if (key === "userId" || key === "partnerUserId") return true;
15475
15750
  if (!key.startsWith("scope:")) return false;
15476
15751
  const ns = key.slice("scope:".length);
15477
15752
  return !FORBIDDEN_SCOPE_NAMESPACES.has(ns) && SCOPE_NAMESPACE_RE.test(ns);
15478
15753
  }
15479
- var SELF_TOKEN_RE2 = /^\$\{\{\s*self\.[a-zA-Z.]+\s*\}\}$/;
15754
+ var DIM_WILDCARD = "*";
15755
+ var ANY_TOKEN_RE = /^\$\{\{\s*any\s*\}\}$/;
15480
15756
  function normalizeClauses(raw) {
15481
15757
  const out = [];
15482
15758
  for (const c of raw) {
@@ -15492,7 +15768,9 @@ function normalizeClauses(raw) {
15492
15768
  dataScope[dim] = values.map((val) => {
15493
15769
  if (val === null) return null;
15494
15770
  const s = String(val);
15495
- 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;
15496
15774
  });
15497
15775
  if (!isRecognizedOwnershipKey(k)) unrecognizedDims.push(k);
15498
15776
  }
@@ -15500,6 +15778,38 @@ function normalizeClauses(raw) {
15500
15778
  }
15501
15779
  return out;
15502
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
+ }
15503
15813
  function buildArchetypes(clauses) {
15504
15814
  const rows = [
15505
15815
  { label: "a record you own alone (private)", owners: { user: SELF } },
@@ -15510,8 +15820,13 @@ function buildArchetypes(clauses) {
15510
15820
  if (c.unrecognizedDims.length > 0) continue;
15511
15821
  for (const [dim, values] of Object.entries(c.dataScope)) {
15512
15822
  if (dim === "user") continue;
15823
+ if (dim === DIM_WILDCARD) continue;
15513
15824
  const set = scopeDims.get(dim) ?? /* @__PURE__ */ new Set();
15514
- 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
+ }
15515
15830
  scopeDims.set(dim, set);
15516
15831
  }
15517
15832
  }
@@ -15530,19 +15845,42 @@ function buildArchetypes(clauses) {
15530
15845
  rows.push({ label: "no owner values at all (tenant-shared)", owners: {} });
15531
15846
  return rows;
15532
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
+ }
15533
15857
  function clauseMatches(clause, arch) {
15534
15858
  if (clause.unrecognizedDims.length > 0) return false;
15859
+ const wildcard = clause.dataScope[DIM_WILDCARD];
15535
15860
  for (const [dim, allowed] of Object.entries(clause.dataScope)) {
15536
- const recordValue = arch.owners[dim];
15537
- const ok = allowed.some((a) => {
15538
- if (a === null) return recordValue === void 0;
15539
- if (a === SELF) return recordValue === SELF;
15540
- return recordValue === a;
15541
- });
15542
- 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;
15543
15872
  }
15544
15873
  return true;
15545
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
+ }
15546
15884
  function explainClauses(clauses) {
15547
15885
  const archetypes = buildArchetypes(clauses);
15548
15886
  return archetypes.map((arch) => {
@@ -15554,15 +15892,34 @@ function explainClauses(clauses) {
15554
15892
  for (const a of c.actions) actions.add(a);
15555
15893
  }
15556
15894
  });
15557
- 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
+ };
15558
15902
  });
15559
15903
  }
15560
15904
  function renderDataScope(ds) {
15561
15905
  const entries = Object.entries(ds);
15562
15906
  if (entries.length === 0) return "(unscoped \u2014 all data in the context)";
15563
- return entries.map(
15564
- ([dim, values]) => `${dim}:[${values.map((v) => v === SELF ? "self" : v === null ? "null" : v).join(", ")}]`
15565
- ).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(", ");
15566
15923
  }
15567
15924
  var SELF_PRINCIPAL_ALIAS2 = "me";
15568
15925
  async function runAccessExplain(opts, deps = {}) {
@@ -15590,6 +15947,7 @@ async function runAccessExplain(opts, deps = {}) {
15590
15947
  }
15591
15948
  const clauses = normalizeClauses(rawClauses);
15592
15949
  const rows = explainClauses(clauses);
15950
+ const undecidable = undecidableDims(clauses);
15593
15951
  if (opts.json) {
15594
15952
  process.stdout.write(
15595
15953
  `${formatJson({
@@ -15606,7 +15964,10 @@ async function runAccessExplain(opts, deps = {}) {
15606
15964
  ...c.unrecognizedDims.length > 0 ? { grantsNothing: true, unrecognizedDims: c.unrecognizedDims } : {}
15607
15965
  })),
15608
15966
  matrix: rows,
15609
- 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."
15610
15971
  })}
15611
15972
  `
15612
15973
  );
@@ -15634,8 +15995,19 @@ async function runAccessExplain(opts, deps = {}) {
15634
15995
  "\n" + formatTable(rows, [
15635
15996
  { header: "record owned by\u2026", get: (r) => r.archetype },
15636
15997
  { header: "granted?", get: (r) => r.clauses.length ? `via clause ${r.clauses.join(", ")}` : "NO" },
15637
- { header: "actions", get: (r) => r.actions.length ? r.actions.join(", ") : "\u2014" }
15638
- ]) + "\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'
15639
16011
  );
15640
16012
  return 0;
15641
16013
  } catch (err) {
@@ -18374,9 +18746,9 @@ function emitJoined(alias, context, principalId, role, keyId, reused) {
18374
18746
 
18375
18747
  // src/build-info.ts
18376
18748
  var BUILD_INFO = {
18377
- cli: true ? "0.12.0" : "dev",
18378
- sdk: true ? "0.37.0" : "dev",
18379
- blueprints: true ? "0.9.0" : "dev"
18749
+ cli: true ? "0.13.0" : "dev",
18750
+ sdk: true ? "0.38.0" : "dev",
18751
+ blueprints: true ? "0.10.0" : "dev"
18380
18752
  };
18381
18753
  function formatBuildInfo() {
18382
18754
  return `vectros ${BUILD_INFO.cli} (sdk ${BUILD_INFO.sdk}, blueprints ${BUILD_INFO.blueprints})`;