@stigmer/server 3.24.0 → 3.26.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/authorization/authorizer.d.ts +10 -4
- package/dist/authorization/authorizer.d.ts.map +1 -1
- package/dist/authorization/authorizer.js +11 -4
- package/dist/authorization/authorizer.js.map +1 -1
- package/dist/authorization/model/index.d.ts +4 -4
- package/dist/authorization/model/index.d.ts.map +1 -1
- package/dist/authorization/model/index.js +2 -0
- package/dist/authorization/model/index.js.map +1 -1
- package/dist/authorization/model/oauth_app.d.ts.map +1 -1
- package/dist/authorization/model/oauth_app.js +3 -2
- package/dist/authorization/model/oauth_app.js.map +1 -1
- package/dist/authorization/model/platform_client.d.ts +2 -0
- package/dist/authorization/model/platform_client.d.ts.map +1 -0
- package/dist/authorization/model/platform_client.js +31 -0
- package/dist/authorization/model/platform_client.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +99 -13
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/list-indexes.d.ts +3 -0
- package/dist/boot/list-indexes.d.ts.map +1 -0
- package/dist/boot/list-indexes.js +24 -0
- package/dist/boot/list-indexes.js.map +1 -0
- package/dist/boot/route-shadowing.d.ts +25 -0
- package/dist/boot/route-shadowing.d.ts.map +1 -0
- package/dist/boot/route-shadowing.js +17 -0
- package/dist/boot/route-shadowing.js.map +1 -0
- package/dist/domain/agentexecution/controller.d.ts.map +1 -1
- package/dist/domain/agentexecution/controller.js +6 -8
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/list-index.d.ts +2 -0
- package/dist/domain/agentexecution/list-index.d.ts.map +1 -0
- package/dist/domain/agentexecution/list-index.js +23 -0
- package/dist/domain/agentexecution/list-index.js.map +1 -0
- package/dist/domain/agentexecution/steps.d.ts +17 -33
- package/dist/domain/agentexecution/steps.d.ts.map +1 -1
- package/dist/domain/agentexecution/steps.js +94 -95
- package/dist/domain/agentexecution/steps.js.map +1 -1
- package/dist/domain/agentexecution/usage.d.ts.map +1 -1
- package/dist/domain/agentexecution/usage.js +37 -14
- package/dist/domain/agentexecution/usage.js.map +1 -1
- package/dist/domain/artifact/controller.d.ts.map +1 -1
- package/dist/domain/artifact/controller.js +27 -15
- package/dist/domain/artifact/controller.js.map +1 -1
- package/dist/domain/artifact/list-index.d.ts +2 -0
- package/dist/domain/artifact/list-index.d.ts.map +1 -0
- package/dist/domain/artifact/list-index.js +21 -0
- package/dist/domain/artifact/list-index.js.map +1 -0
- package/dist/domain/iampolicy/membership.d.ts +6 -1
- package/dist/domain/iampolicy/membership.d.ts.map +1 -1
- package/dist/domain/iampolicy/membership.js +8 -2
- package/dist/domain/iampolicy/membership.js.map +1 -1
- package/dist/domain/identityaccount/constants.d.ts +13 -0
- package/dist/domain/identityaccount/constants.d.ts.map +1 -1
- package/dist/domain/identityaccount/constants.js +17 -0
- package/dist/domain/identityaccount/constants.js.map +1 -1
- package/dist/domain/identityaccount/controller.d.ts +3 -1
- package/dist/domain/identityaccount/controller.d.ts.map +1 -1
- package/dist/domain/identityaccount/controller.js +15 -6
- package/dist/domain/identityaccount/controller.js.map +1 -1
- package/dist/domain/identityaccount/operator.d.ts.map +1 -1
- package/dist/domain/identityaccount/operator.js +1 -0
- package/dist/domain/identityaccount/operator.js.map +1 -1
- package/dist/domain/identityaccount/provisioning.d.ts +18 -0
- package/dist/domain/identityaccount/provisioning.d.ts.map +1 -1
- package/dist/domain/identityaccount/provisioning.js +1 -0
- package/dist/domain/identityaccount/provisioning.js.map +1 -1
- package/dist/domain/identityaccount/resource-store.js +9 -5
- package/dist/domain/identityaccount/resource-store.js.map +1 -1
- package/dist/domain/identityaccount/steps.d.ts +11 -2
- package/dist/domain/identityaccount/steps.d.ts.map +1 -1
- package/dist/domain/identityaccount/steps.js +42 -4
- package/dist/domain/identityaccount/steps.js.map +1 -1
- package/dist/domain/identityaccount/store-contract.d.ts.map +1 -1
- package/dist/domain/identityaccount/store-contract.js +33 -0
- package/dist/domain/identityaccount/store-contract.js.map +1 -1
- package/dist/domain/identityaccount/store.d.ts +7 -5
- package/dist/domain/identityaccount/store.d.ts.map +1 -1
- package/dist/domain/identityaccount/store.js.map +1 -1
- package/dist/domain/platform/controller.d.ts +10 -0
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js +2 -1
- package/dist/domain/platform/controller.js.map +1 -1
- package/dist/domain/platformclient/constants.d.ts +60 -0
- package/dist/domain/platformclient/constants.d.ts.map +1 -0
- package/dist/domain/platformclient/constants.js +78 -0
- package/dist/domain/platformclient/constants.js.map +1 -0
- package/dist/domain/platformclient/controller.d.ts +59 -0
- package/dist/domain/platformclient/controller.d.ts.map +1 -0
- package/dist/domain/platformclient/controller.js +191 -0
- package/dist/domain/platformclient/controller.js.map +1 -0
- package/dist/domain/platformclient/credentials.d.ts +13 -0
- package/dist/domain/platformclient/credentials.d.ts.map +1 -0
- package/dist/domain/platformclient/credentials.js +56 -0
- package/dist/domain/platformclient/credentials.js.map +1 -0
- package/dist/domain/platformclient/mint.d.ts +23 -0
- package/dist/domain/platformclient/mint.d.ts.map +1 -0
- package/dist/domain/platformclient/mint.js +254 -0
- package/dist/domain/platformclient/mint.js.map +1 -0
- package/dist/domain/platformclient/origin-guard.d.ts +9 -0
- package/dist/domain/platformclient/origin-guard.d.ts.map +1 -0
- package/dist/domain/platformclient/origin-guard.js +81 -0
- package/dist/domain/platformclient/origin-guard.js.map +1 -0
- package/dist/domain/platformclient/resource-store.d.ts +4 -0
- package/dist/domain/platformclient/resource-store.d.ts.map +1 -0
- package/dist/domain/platformclient/resource-store.js +117 -0
- package/dist/domain/platformclient/resource-store.js.map +1 -0
- package/dist/domain/platformclient/steps.d.ts +51 -0
- package/dist/domain/platformclient/steps.d.ts.map +1 -0
- package/dist/domain/platformclient/steps.js +339 -0
- package/dist/domain/platformclient/steps.js.map +1 -0
- package/dist/domain/platformclient/store-contract.d.ts +8 -0
- package/dist/domain/platformclient/store-contract.d.ts.map +1 -0
- package/dist/domain/platformclient/store-contract.js +208 -0
- package/dist/domain/platformclient/store-contract.js.map +1 -0
- package/dist/domain/platformclient/store.d.ts +58 -0
- package/dist/domain/platformclient/store.d.ts.map +1 -0
- package/dist/domain/platformclient/store.js +13 -0
- package/dist/domain/platformclient/store.js.map +1 -0
- package/dist/domain/platformclient/system-managed.d.ts +18 -0
- package/dist/domain/platformclient/system-managed.d.ts.map +1 -0
- package/dist/domain/platformclient/system-managed.js +80 -0
- package/dist/domain/platformclient/system-managed.js.map +1 -0
- package/dist/domain/platformclient/token-controller.d.ts +11 -0
- package/dist/domain/platformclient/token-controller.d.ts.map +1 -0
- package/dist/domain/platformclient/token-controller.js +32 -0
- package/dist/domain/platformclient/token-controller.js.map +1 -0
- package/dist/domain/platformclient/verifier.d.ts +10 -0
- package/dist/domain/platformclient/verifier.d.ts.map +1 -0
- package/dist/domain/platformclient/verifier.js +76 -0
- package/dist/domain/platformclient/verifier.js.map +1 -0
- package/dist/domain/session/list-index.d.ts +4 -0
- package/dist/domain/session/list-index.d.ts.map +1 -0
- package/dist/domain/session/list-index.js +26 -0
- package/dist/domain/session/list-index.js.map +1 -0
- package/dist/domain/session/steps.d.ts +7 -8
- package/dist/domain/session/steps.d.ts.map +1 -1
- package/dist/domain/session/steps.js +76 -78
- package/dist/domain/session/steps.js.map +1 -1
- package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
- package/dist/domain/workflowexecution/controller.js +53 -55
- package/dist/domain/workflowexecution/controller.js.map +1 -1
- package/dist/domain/workflowexecution/execution-filter.d.ts +4 -0
- package/dist/domain/workflowexecution/execution-filter.d.ts.map +1 -1
- package/dist/domain/workflowexecution/execution-filter.js +11 -5
- package/dist/domain/workflowexecution/execution-filter.js.map +1 -1
- package/dist/domain/workflowexecution/get-execution-summary.d.ts.map +1 -1
- package/dist/domain/workflowexecution/get-execution-summary.js +23 -17
- package/dist/domain/workflowexecution/get-execution-summary.js.map +1 -1
- package/dist/domain/workflowexecution/list-index.d.ts +2 -0
- package/dist/domain/workflowexecution/list-index.d.ts.map +1 -0
- package/dist/domain/workflowexecution/list-index.js +21 -0
- package/dist/domain/workflowexecution/list-index.js.map +1 -0
- package/dist/domain/workflowexecution/list-pending-approvals.d.ts.map +1 -1
- package/dist/domain/workflowexecution/list-pending-approvals.js +69 -32
- package/dist/domain/workflowexecution/list-pending-approvals.js.map +1 -1
- package/dist/domain/workflowexecution/queries.d.ts +30 -4
- package/dist/domain/workflowexecution/queries.d.ts.map +1 -1
- package/dist/domain/workflowexecution/queries.js +76 -22
- package/dist/domain/workflowexecution/queries.js.map +1 -1
- package/dist/encryption/key-manager.d.ts +20 -3
- package/dist/encryption/key-manager.d.ts.map +1 -1
- package/dist/encryption/key-manager.js +64 -42
- package/dist/encryption/key-manager.js.map +1 -1
- package/dist/extensions/caller-guards.d.ts +8 -3
- package/dist/extensions/caller-guards.d.ts.map +1 -1
- package/dist/extensions/drivers.d.ts +37 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/guest-token-minting.d.ts +26 -0
- package/dist/extensions/guest-token-minting.d.ts.map +1 -0
- package/dist/extensions/guest-token-minting.js +2 -0
- package/dist/extensions/guest-token-minting.js.map +1 -0
- package/dist/extensions/identity.d.ts +6 -4
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js.map +1 -1
- package/dist/extensions/list-read-scope.d.ts +9 -12
- package/dist/extensions/list-read-scope.d.ts.map +1 -1
- package/dist/extensions/list-read-scope.js.map +1 -1
- package/dist/extensions/registry.d.ts +30 -1
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +34 -1
- package/dist/extensions/registry.js.map +1 -1
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/interceptors/auth.d.ts +14 -0
- package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
- package/dist/pipeline/interceptors/auth.js +24 -0
- package/dist/pipeline/interceptors/auth.js.map +1 -1
- package/dist/pipeline/steps/build-update-state.d.ts +11 -1
- package/dist/pipeline/steps/build-update-state.d.ts.map +1 -1
- package/dist/pipeline/steps/build-update-state.js +5 -2
- package/dist/pipeline/steps/build-update-state.js.map +1 -1
- package/dist/pipeline/steps/list-page.d.ts +63 -0
- package/dist/pipeline/steps/list-page.d.ts.map +1 -0
- package/dist/pipeline/steps/list-page.js +187 -0
- package/dist/pipeline/steps/list-page.js.map +1 -0
- package/dist/platformtoken/envelope.d.ts +100 -0
- package/dist/platformtoken/envelope.d.ts.map +1 -0
- package/dist/platformtoken/envelope.js +184 -0
- package/dist/platformtoken/envelope.js.map +1 -0
- package/dist/platformtoken/key-ring.d.ts +85 -0
- package/dist/platformtoken/key-ring.d.ts.map +1 -0
- package/dist/platformtoken/key-ring.js +164 -0
- package/dist/platformtoken/key-ring.js.map +1 -0
- package/dist/query/activity/handler.d.ts +6 -7
- package/dist/query/activity/handler.d.ts.map +1 -1
- package/dist/query/activity/handler.js +42 -59
- package/dist/query/activity/handler.js.map +1 -1
- package/dist/store/interface.d.ts +29 -0
- package/dist/store/interface.d.ts.map +1 -1
- package/dist/store/interface.js.map +1 -1
- package/dist/store/list-index.d.ts +123 -0
- package/dist/store/list-index.d.ts.map +1 -0
- package/dist/store/list-index.js +277 -0
- package/dist/store/list-index.js.map +1 -0
- package/dist/store/logger.d.ts +3 -0
- package/dist/store/logger.d.ts.map +1 -1
- package/dist/store/logger.js +5 -1
- package/dist/store/logger.js.map +1 -1
- package/dist/store/postgres/migrations.d.ts +10 -6
- package/dist/store/postgres/migrations.d.ts.map +1 -1
- package/dist/store/postgres/migrations.js +64 -6
- package/dist/store/postgres/migrations.js.map +1 -1
- package/dist/store/postgres/store.d.ts +26 -5
- package/dist/store/postgres/store.d.ts.map +1 -1
- package/dist/store/postgres/store.js +377 -22
- package/dist/store/postgres/store.js.map +1 -1
- package/dist/store/sqlite/migrations.d.ts +11 -5
- package/dist/store/sqlite/migrations.d.ts.map +1 -1
- package/dist/store/sqlite/migrations.js +50 -4
- package/dist/store/sqlite/migrations.js.map +1 -1
- package/dist/store/sqlite/store.d.ts +18 -3
- package/dist/store/sqlite/store.d.ts.map +1 -1
- package/dist/store/sqlite/store.js +321 -15
- package/dist/store/sqlite/store.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/README.md +1 -1
- package/src/authorization/__tests__/authorizer.test.ts +6 -2
- package/src/authorization/__tests__/drivers.ts +6 -1
- package/src/authorization/__tests__/wire-permissions.test.ts +1 -0
- package/src/authorization/authorizer.ts +11 -4
- package/src/authorization/model/__tests__/registry.test.ts +8 -1
- package/src/authorization/model/index.ts +6 -4
- package/src/authorization/model/oauth_app.ts +3 -2
- package/src/authorization/model/platform_client.ts +43 -0
- package/src/boot/__tests__/auth-enabled-composition.test.ts +2 -1
- package/src/boot/__tests__/list-indexes.test.ts +65 -0
- package/src/boot/__tests__/route-shadowing.test.ts +67 -0
- package/src/boot/compose.ts +113 -18
- package/src/boot/list-indexes.ts +25 -0
- package/src/boot/route-shadowing.ts +47 -0
- package/src/domain/agentexecution/__tests__/agentexecution.test.ts +85 -3
- package/src/domain/agentexecution/controller.ts +8 -10
- package/src/domain/agentexecution/list-index.ts +24 -0
- package/src/domain/agentexecution/steps.ts +118 -115
- package/src/domain/agentexecution/usage.ts +38 -17
- package/src/domain/artifact/controller.ts +28 -18
- package/src/domain/artifact/list-index.ts +22 -0
- package/src/domain/iampolicy/__tests__/membership.test.ts +7 -0
- package/src/domain/iampolicy/membership.ts +8 -2
- package/src/domain/identityaccount/__tests__/platform-client-accounts.test.ts +124 -0
- package/src/domain/identityaccount/__tests__/resource-store.test.ts +1 -0
- package/src/domain/identityaccount/constants.ts +20 -0
- package/src/domain/identityaccount/controller.ts +16 -4
- package/src/domain/identityaccount/operator.ts +1 -0
- package/src/domain/identityaccount/provisioning.ts +17 -0
- package/src/domain/identityaccount/resource-store.ts +9 -5
- package/src/domain/identityaccount/steps.ts +58 -3
- package/src/domain/identityaccount/store-contract.ts +51 -0
- package/src/domain/identityaccount/store.ts +7 -5
- package/src/domain/platform/__tests__/platform.test.ts +7 -1
- package/src/domain/platform/controller.ts +12 -1
- package/src/domain/platformclient/__tests__/controller.test.ts +397 -0
- package/src/domain/platformclient/__tests__/credentials.test.ts +46 -0
- package/src/domain/platformclient/__tests__/mint.test.ts +415 -0
- package/src/domain/platformclient/__tests__/resource-store.test.ts +140 -0
- package/src/domain/platformclient/__tests__/system-managed.test.ts +101 -0
- package/src/domain/platformclient/__tests__/verifier-and-guard.test.ts +207 -0
- package/src/domain/platformclient/constants.ts +120 -0
- package/src/domain/platformclient/controller.ts +449 -0
- package/src/domain/platformclient/credentials.ts +68 -0
- package/src/domain/platformclient/mint.ts +412 -0
- package/src/domain/platformclient/origin-guard.ts +117 -0
- package/src/domain/platformclient/resource-store.ts +150 -0
- package/src/domain/platformclient/steps.ts +472 -0
- package/src/domain/platformclient/store-contract.ts +318 -0
- package/src/domain/platformclient/store.ts +63 -0
- package/src/domain/platformclient/system-managed.ts +115 -0
- package/src/domain/platformclient/token-controller.ts +49 -0
- package/src/domain/platformclient/verifier.ts +106 -0
- package/src/domain/session/list-index.ts +28 -0
- package/src/domain/session/steps.ts +108 -123
- package/src/domain/workflowexecution/__tests__/list-read-scope-summaries.test.ts +155 -99
- package/src/domain/workflowexecution/__tests__/workflowexecution.test.ts +193 -31
- package/src/domain/workflowexecution/controller.ts +54 -82
- package/src/domain/workflowexecution/execution-filter.ts +20 -7
- package/src/domain/workflowexecution/get-execution-summary.ts +31 -24
- package/src/domain/workflowexecution/list-index.ts +22 -0
- package/src/domain/workflowexecution/list-pending-approvals.ts +82 -25
- package/src/domain/workflowexecution/queries.ts +134 -23
- package/src/encryption/key-manager.ts +87 -44
- package/src/extensions/__tests__/extension-composition.test.ts +3 -2
- package/src/extensions/__tests__/platform-client-composed.test.ts +288 -0
- package/src/extensions/__tests__/registry.test.ts +4 -1
- package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
- package/src/extensions/caller-guards.ts +8 -3
- package/src/extensions/drivers.ts +37 -0
- package/src/extensions/guest-token-minting.ts +32 -0
- package/src/extensions/identity.ts +6 -4
- package/src/extensions/list-read-scope.ts +9 -12
- package/src/extensions/registry.ts +77 -3
- package/src/index.ts +65 -1
- package/src/pipeline/interceptors/auth.ts +27 -0
- package/src/pipeline/steps/__tests__/list-page.test.ts +186 -0
- package/src/pipeline/steps/__tests__/references.test.ts +2 -0
- package/src/pipeline/steps/build-update-state.ts +5 -2
- package/src/pipeline/steps/list-page.ts +284 -0
- package/src/platformtoken/__tests__/envelope.test.ts +277 -0
- package/src/platformtoken/__tests__/key-ring.test.ts +205 -0
- package/src/platformtoken/envelope.ts +276 -0
- package/src/platformtoken/key-ring.ts +266 -0
- package/src/query/activity/handler.ts +57 -78
- package/src/runnerauth/__tests__/runner-subject-composed.test.ts +9 -3
- package/src/store/README.md +51 -32
- package/src/store/__tests__/list-index.measure.test.ts +293 -0
- package/src/store/__tests__/list-index.test.ts +328 -0
- package/src/store/__tests__/store-contract.ts +346 -3
- package/src/store/interface.ts +39 -0
- package/src/store/list-index.ts +465 -0
- package/src/store/logger.ts +8 -1
- package/src/store/postgres/__tests__/list-index-repair.test.ts +133 -0
- package/src/store/postgres/__tests__/migrations.test.ts +27 -19
- package/src/store/postgres/__tests__/store-contract.test.ts +50 -3
- package/src/store/postgres/migrations.ts +69 -7
- package/src/store/postgres/store.ts +485 -29
- package/src/store/sqlite/__tests__/migrations.test.ts +51 -7
- package/src/store/sqlite/__tests__/store-contract.test.ts +70 -16
- package/src/store/sqlite/__tests__/support.ts +12 -3
- package/src/store/sqlite/migrations.ts +57 -6
- package/src/store/sqlite/store.ts +462 -21
package/src/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ export type {
|
|
|
59
59
|
export type {
|
|
60
60
|
ExtensionServiceRegistration,
|
|
61
61
|
ResolvedExtensions,
|
|
62
|
+
ResolvedServiceRegistration,
|
|
62
63
|
ServerExtension,
|
|
63
64
|
} from "./extensions/registry.js";
|
|
64
65
|
export type {
|
|
@@ -166,6 +167,60 @@ export type { AuthorizationQueryEngine } from "./extensions/authorization-querie
|
|
|
166
167
|
export type { IdentityFederation } from "./extensions/identity-federation.js";
|
|
167
168
|
export type { AccountsBySubject } from "./domain/identityaccount/resolve.js";
|
|
168
169
|
export { identityIdForSubject } from "./domain/identityaccount/resolve.js";
|
|
170
|
+
// The PlatformClient seams: the store PORT a composition drives the domain
|
|
171
|
+
// through (drivers.platformClientStore; a driver throws
|
|
172
|
+
// DuplicatePlatformClientError for a held id, slug or client_id) and its
|
|
173
|
+
// vitest-free contract kit; the guest-token capability
|
|
174
|
+
// (drivers.guestTokenMinting — the one token method only an edition that
|
|
175
|
+
// hosts shared-agent pages serves); and the builder of the system-managed
|
|
176
|
+
// client such an edition keeps per organization under the reserved slug,
|
|
177
|
+
// so the tokens it signs for guests and schedule fires name a real client.
|
|
178
|
+
export type { PlatformClientStore } from "./domain/platformclient/store.js";
|
|
179
|
+
export { DuplicatePlatformClientError } from "./domain/platformclient/store.js";
|
|
180
|
+
export type {
|
|
181
|
+
PlatformClientStoreContractCase,
|
|
182
|
+
PlatformClientStoreContractFixture,
|
|
183
|
+
} from "./domain/platformclient/store-contract.js";
|
|
184
|
+
export { platformClientStoreContract } from "./domain/platformclient/store-contract.js";
|
|
185
|
+
export type { GuestTokenMinting } from "./extensions/guest-token-minting.js";
|
|
186
|
+
export type { SystemManagedPlatformClientInput } from "./domain/platformclient/system-managed.js";
|
|
187
|
+
export { newSystemManagedPlatformClient } from "./domain/platformclient/system-managed.js";
|
|
188
|
+
export { SYSTEM_SHARE_CLIENT_SLUG } from "./domain/platformclient/constants.js";
|
|
189
|
+
// The platform-token envelope and its key ring: the RS256 JWT every token
|
|
190
|
+
// the server signs for itself rides. A composition supplies its ring
|
|
191
|
+
// (drivers.platformTokenKeys, built from its PEMs), signs its own typed
|
|
192
|
+
// lanes through the envelope — each with its own lifetime when it is not
|
|
193
|
+
// the ring's default, the expiry handed back — and verifies them with the
|
|
194
|
+
// same function and refusal copy open source's user-token lane uses.
|
|
195
|
+
export type {
|
|
196
|
+
PlatformTokenClaimValue,
|
|
197
|
+
PlatformTokenRefusal,
|
|
198
|
+
PlatformTokenSigningOptions,
|
|
199
|
+
PlatformTokenVerification,
|
|
200
|
+
SignedPlatformToken,
|
|
201
|
+
VerifiedPlatformToken,
|
|
202
|
+
} from "./platformtoken/envelope.js";
|
|
203
|
+
export {
|
|
204
|
+
PLATFORM_TOKEN_ISSUER,
|
|
205
|
+
PLATFORM_TOKEN_REFUSAL_MESSAGES,
|
|
206
|
+
TOKEN_TYPE_CLAIM,
|
|
207
|
+
decodeVerifiedPlatformTokenPayload,
|
|
208
|
+
platformTokenRefusalError,
|
|
209
|
+
signPlatformToken,
|
|
210
|
+
stringClaim,
|
|
211
|
+
verifyPlatformToken,
|
|
212
|
+
} from "./platformtoken/envelope.js";
|
|
213
|
+
export type {
|
|
214
|
+
PlatformTokenKeyMaterial,
|
|
215
|
+
PlatformTokenKeyRing,
|
|
216
|
+
PlatformTokenSigner,
|
|
217
|
+
SigningPlatformTokenKeyRing,
|
|
218
|
+
} from "./platformtoken/key-ring.js";
|
|
219
|
+
export {
|
|
220
|
+
DEFAULT_PLATFORM_TOKEN_TTL_SECONDS,
|
|
221
|
+
canSign,
|
|
222
|
+
platformTokenKeyRingFromPem,
|
|
223
|
+
} from "./platformtoken/key-ring.js";
|
|
169
224
|
// The built-in authorizer's model seams: the kind
|
|
170
225
|
// declarations (transcripts of the cloud's `.fga` files), the evaluator,
|
|
171
226
|
// and the OpenFGA store-test kit — exported so the cloud's drift test
|
|
@@ -325,11 +380,20 @@ export { newResolveSlugStep } from "./pipeline/steps/slug.js";
|
|
|
325
380
|
// The driver interfaces and the store-fault classes the ratified mapping
|
|
326
381
|
// keys on (typed not-found → NotFound; anything else rethrows as an
|
|
327
382
|
// infrastructure fault — the guidelines' instanceof idiom).
|
|
328
|
-
export type { Store } from "./store/interface.js";
|
|
383
|
+
export type { Store, StoreOpenOptions } from "./store/interface.js";
|
|
329
384
|
export {
|
|
330
385
|
AuditNotFoundError,
|
|
331
386
|
ResourceNotFoundError,
|
|
332
387
|
} from "./store/interface.js";
|
|
388
|
+
// The list index's read shapes, which `Store.queryResources` speaks
|
|
389
|
+
// (store/list-index.ts). Declaring an index stays internal: the list is
|
|
390
|
+
// the composition root's (boot/list-indexes.ts), one per server.
|
|
391
|
+
export type {
|
|
392
|
+
ListIndexCursor,
|
|
393
|
+
ListIndexDeclaration,
|
|
394
|
+
ListIndexQuery,
|
|
395
|
+
ListIndexRow,
|
|
396
|
+
} from "./store/list-index.js";
|
|
333
397
|
// The maintenance-surface row shape (20260830.04 Stage 1, ruling Q3):
|
|
334
398
|
// what findResourcesRawOrderedAfter pages and what
|
|
335
399
|
// replaceResourceDataIfUnchanged guards on — the secret-convergence
|
|
@@ -216,6 +216,33 @@ export function trustedLocalIdentityFor(operator: {
|
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* The server acting for a principal it has authenticated some other way —
|
|
221
|
+
* the one construction of an `internal` caller outside the in-process
|
|
222
|
+
* transport's own stamp. Its consumer is the PlatformClient mint
|
|
223
|
+
* (domain/platformclient/mint.ts): the request is public and tokenless
|
|
224
|
+
* (so the chain stamped the trusted-local operator), the client proved
|
|
225
|
+
* itself with its secret, and the end-user account and role the mint
|
|
226
|
+
* creates must name the CLIENT as their actor, never the operator.
|
|
227
|
+
* `internal` because the server is the one acting — the create path's
|
|
228
|
+
* Authorize skips it, as for every server-composed request — and never
|
|
229
|
+
* reachable from the wire: no verifier produces it, and the serving
|
|
230
|
+
* chassis overwrites position 1 on every request.
|
|
231
|
+
*/
|
|
232
|
+
export function serverActingFor(principalId: string): CallerIdentity {
|
|
233
|
+
if (principalId === "") {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"the server acts for a named principal — refusing to build an internal caller with none",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
identityId: principalId,
|
|
240
|
+
callerClass: "internal",
|
|
241
|
+
issuer: "",
|
|
242
|
+
rawToken: "",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
219
246
|
/**
|
|
220
247
|
* The serving chain's position-1 identity source: walks the composed
|
|
221
248
|
* verifiers in order (OSS entries first, extension entries after, in
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the page loop every paged list lane shares (../list-page.ts),
|
|
3
|
+
* against a real store opened with the server's list indexes:
|
|
4
|
+
* `page_size` 0 reads the whole list with no token; a positive size walks
|
|
5
|
+
* every row in the store's order with no gap and no duplicate, the last
|
|
6
|
+
* page carrying no token (also when the rows end exactly on a page); a
|
|
7
|
+
* scope that refuses rows still fills pages and the cut resumes after the
|
|
8
|
+
* last row returned; a caller who may see nothing gets short pages bounded
|
|
9
|
+
* by the examine budget, each with a token, until the rows run out; a size
|
|
10
|
+
* above the contract's maximum is capped; and the token's refusals — a
|
|
11
|
+
* negative size, malformed text, a token issued for another request.
|
|
12
|
+
*/
|
|
13
|
+
import { create } from "@bufbuild/protobuf";
|
|
14
|
+
import { ConnectError, Code } from "@connectrpc/connect";
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
16
|
+
|
|
17
|
+
import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
18
|
+
import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
19
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
20
|
+
import { fromBinary } from "@bufbuild/protobuf";
|
|
21
|
+
|
|
22
|
+
import { sessionListIndex } from "../../../domain/session/list-index.js";
|
|
23
|
+
import { tempStore } from "../../../store/sqlite/__tests__/support.js";
|
|
24
|
+
import type { TempStore } from "../../../store/sqlite/__tests__/support.js";
|
|
25
|
+
import {
|
|
26
|
+
LIST_PAGE_MAX_SIZE,
|
|
27
|
+
examineBudgetOf,
|
|
28
|
+
listPageFingerprint,
|
|
29
|
+
readListPage,
|
|
30
|
+
} from "../list-page.js";
|
|
31
|
+
import type { ListPageParams } from "../list-page.js";
|
|
32
|
+
|
|
33
|
+
let temp: TempStore;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
temp = tempStore();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
await temp.cleanup();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function seed(count: number): Promise<string[]> {
|
|
44
|
+
const ids: string[] = [];
|
|
45
|
+
for (let i = 0; i < count; i++) {
|
|
46
|
+
const id = `ses_${String(i).padStart(4, "0")}`;
|
|
47
|
+
ids.push(id);
|
|
48
|
+
await temp.store.saveResource(
|
|
49
|
+
ApiResourceKind.session,
|
|
50
|
+
id,
|
|
51
|
+
SessionSchema,
|
|
52
|
+
create(SessionSchema, {
|
|
53
|
+
metadata: { id, org: "acme" },
|
|
54
|
+
status: {
|
|
55
|
+
audit: {
|
|
56
|
+
specAudit: { createdAt: { seconds: BigInt(1_000 + i), nanos: 0 } },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
// Newest first: the highest creation instant first.
|
|
63
|
+
return ids.reverse();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function params(
|
|
67
|
+
pageSize: number,
|
|
68
|
+
pageToken: string,
|
|
69
|
+
scope: (rows: Session[]) => Session[] = (rows) => rows,
|
|
70
|
+
): ListPageParams<Session, "agent_instance" | "channel"> {
|
|
71
|
+
return {
|
|
72
|
+
store: temp.store,
|
|
73
|
+
declaration: sessionListIndex,
|
|
74
|
+
query: { org: "acme" },
|
|
75
|
+
request: { pageSize, pageToken },
|
|
76
|
+
fingerprint: listPageFingerprint({ lane: "test", org: "acme" }),
|
|
77
|
+
decode: (data) => fromBinary(SessionSchema, data),
|
|
78
|
+
scope: (rows) => Promise.resolve(scope(rows)),
|
|
79
|
+
failure: "failed to list sessions",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function walk(
|
|
84
|
+
pageSize: number,
|
|
85
|
+
scope?: (rows: Session[]) => Session[],
|
|
86
|
+
): Promise<{ ids: string[]; pages: number }> {
|
|
87
|
+
const ids: string[] = [];
|
|
88
|
+
let token = "";
|
|
89
|
+
let pages = 0;
|
|
90
|
+
do {
|
|
91
|
+
const page = await readListPage(params(pageSize, token, scope));
|
|
92
|
+
ids.push(...page.entries.map((s) => s.metadata!.id));
|
|
93
|
+
token = page.nextPageToken;
|
|
94
|
+
pages += 1;
|
|
95
|
+
} while (token !== "");
|
|
96
|
+
return { ids, pages };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function refusal(promise: Promise<unknown>): Promise<ConnectError> {
|
|
100
|
+
try {
|
|
101
|
+
await promise;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof ConnectError) {
|
|
104
|
+
return error;
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
throw new Error("expected a refusal");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
describe("readListPage", () => {
|
|
112
|
+
it("reads the whole list with no token at page_size 0", async () => {
|
|
113
|
+
const all = await seed(7);
|
|
114
|
+
const page = await readListPage(params(0, ""));
|
|
115
|
+
expect(page.entries.map((s) => s.metadata!.id)).toEqual(all);
|
|
116
|
+
expect(page.nextPageToken).toBe("");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("walks every row with no gap and no duplicate, the last page without a token", async () => {
|
|
120
|
+
const all = await seed(7);
|
|
121
|
+
const { ids, pages } = await walk(3);
|
|
122
|
+
expect(ids).toEqual(all);
|
|
123
|
+
expect(pages).toBe(3);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("issues no token when the rows end exactly on a page", async () => {
|
|
127
|
+
const all = await seed(6);
|
|
128
|
+
const { ids, pages } = await walk(3);
|
|
129
|
+
expect(ids).toEqual(all);
|
|
130
|
+
expect(pages).toBe(2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("fills pages around rows the scope refuses and resumes after the last row returned", async () => {
|
|
134
|
+
const all = await seed(12);
|
|
135
|
+
const refused = new Set(["ses_0010", "ses_0007", "ses_0006", "ses_0001"]);
|
|
136
|
+
const scope = (rows: Session[]) =>
|
|
137
|
+
rows.filter((s) => !refused.has(s.metadata!.id));
|
|
138
|
+
const first = await readListPage(params(3, "", scope));
|
|
139
|
+
expect(first.entries).toHaveLength(3);
|
|
140
|
+
const { ids } = await walk(3, scope);
|
|
141
|
+
expect(ids).toEqual(all.filter((id) => !refused.has(id)));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("bounds what a caller who may see nothing examines, with a token until the rows run out", async () => {
|
|
145
|
+
await seed(250);
|
|
146
|
+
const noneVisible = () => [];
|
|
147
|
+
const first = await readListPage(params(20, "", noneVisible));
|
|
148
|
+
expect(first.entries).toEqual([]);
|
|
149
|
+
expect(first.nextPageToken).not.toBe("");
|
|
150
|
+
expect(examineBudgetOf(20)).toBe(100);
|
|
151
|
+
const { ids, pages } = await walk(20, noneVisible);
|
|
152
|
+
expect(ids).toEqual([]);
|
|
153
|
+
expect(pages).toBe(3);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("caps a page at the contract's maximum", async () => {
|
|
157
|
+
await seed(LIST_PAGE_MAX_SIZE + 5);
|
|
158
|
+
const page = await readListPage(params(1_000, ""));
|
|
159
|
+
expect(page.entries).toHaveLength(LIST_PAGE_MAX_SIZE);
|
|
160
|
+
expect(page.nextPageToken).not.toBe("");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("refuses a negative size, a malformed token and a token issued for another request", async () => {
|
|
164
|
+
await seed(4);
|
|
165
|
+
expect((await refusal(readListPage(params(-1, "")))).code).toBe(
|
|
166
|
+
Code.InvalidArgument,
|
|
167
|
+
);
|
|
168
|
+
const bad = await refusal(readListPage(params(2, "not a token")));
|
|
169
|
+
expect([bad.code, bad.rawMessage]).toEqual([
|
|
170
|
+
Code.InvalidArgument,
|
|
171
|
+
"invalid page_token",
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
const token = (await readListPage(params(2, ""))).nextPageToken;
|
|
175
|
+
const other = await refusal(
|
|
176
|
+
readListPage({
|
|
177
|
+
...params(2, token),
|
|
178
|
+
fingerprint: listPageFingerprint({ lane: "test", org: "other" }),
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
expect([other.code, other.rawMessage]).toEqual([
|
|
182
|
+
Code.InvalidArgument,
|
|
183
|
+
"page_token was issued for a different request",
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -39,6 +39,7 @@ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/
|
|
|
39
39
|
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
40
40
|
import { reference_kind } from "@stigmer/protos/ai/stigmer/commons/apiresource/field_options_pb";
|
|
41
41
|
import { UpdateVisibilityInputSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
42
|
+
import { PlatformClientSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
42
43
|
|
|
43
44
|
import type { Store } from "../../../store/interface.js";
|
|
44
45
|
import { SqliteStore } from "../../../store/sqlite/store.js";
|
|
@@ -97,6 +98,7 @@ const SCHEMAS_UNDER_THE_RULE: ReadonlyArray<DescMessage> = [
|
|
|
97
98
|
AgentExecutionSchema,
|
|
98
99
|
WorkflowExecutionSchema,
|
|
99
100
|
ExecutionContextSchema,
|
|
101
|
+
PlatformClientSchema,
|
|
100
102
|
AgentCallTaskConfigSchema,
|
|
101
103
|
];
|
|
102
104
|
|
|
@@ -115,9 +115,12 @@ function copyStatusFromExisting(
|
|
|
115
115
|
/**
|
|
116
116
|
* Go updateAuditFieldsReflect: spec_audit keeps the existing creation
|
|
117
117
|
* identity with a fresh update stamp; status_audit is fully current (the
|
|
118
|
-
* status was rebuilt); both carry event "updated".
|
|
118
|
+
* status was rebuilt); both carry event "updated". Exported for the one
|
|
119
|
+
* write that changes a stored resource outside this step — a
|
|
120
|
+
* PlatformClient secret rotation (domain/platformclient/steps.ts) — so an
|
|
121
|
+
* update's audit has one shape whichever step writes it.
|
|
119
122
|
*/
|
|
120
|
-
function updateAuditFields(
|
|
123
|
+
export function updateAuditFields(
|
|
121
124
|
schema: DescMessage,
|
|
122
125
|
resource: Message,
|
|
123
126
|
existing: Message,
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One page of a list lane read through the list index — the one
|
|
3
|
+
* implementation of the page loop every paged lane calls, so the rules
|
|
4
|
+
* below hold everywhere by construction:
|
|
5
|
+
*
|
|
6
|
+
* - The store narrows by the request's own predicates (organization, key,
|
|
7
|
+
* creation window) and orders newest first (store/list-index.ts). The
|
|
8
|
+
* lane's per-row filter runs on each batch, then the read scope, which
|
|
9
|
+
* stays the last per-row predicate (extensions/list-read-scope.ts);
|
|
10
|
+
* neither reorders, so a page is in the store's order.
|
|
11
|
+
* - `page_size` 0 reads every matching row and returns no token, the
|
|
12
|
+
* behaviour every list lane had before it paged; a positive size is
|
|
13
|
+
* capped at `LIST_PAGE_MAX_SIZE`.
|
|
14
|
+
* - A page fills batch by batch until it is full, the rows run out, or
|
|
15
|
+
* `examineBudgetOf(size)` rows have been examined. A caller who may see
|
|
16
|
+
* few of the organization's rows gets a short page, possibly empty, with
|
|
17
|
+
* a token (the timeline's cursor contract, agentchannel/v1
|
|
18
|
+
* conversation_io.proto), never an unbounded walk.
|
|
19
|
+
* - The token's cursor is the last row RETURNED when the page was cut,
|
|
20
|
+
* else the last row EXAMINED, so no row is skipped and no refused row is
|
|
21
|
+
* read twice. The token is opaque and versioned, carries a fingerprint
|
|
22
|
+
* of the request's other fields (a mismatch is refused), and carries no
|
|
23
|
+
* authority: every page re-runs the scope.
|
|
24
|
+
*
|
|
25
|
+
* Proven by __tests__/list-page.test.ts (the loop, the budget, the cut,
|
|
26
|
+
* the token) and end-to-end by the paging arms of the conformance suites.
|
|
27
|
+
*/
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
|
|
30
|
+
import type { Store } from "../../store/interface.js";
|
|
31
|
+
import type {
|
|
32
|
+
ListIndexCursor,
|
|
33
|
+
ListIndexDeclaration,
|
|
34
|
+
ListIndexQuery,
|
|
35
|
+
ListIndexRow,
|
|
36
|
+
} from "../../store/list-index.js";
|
|
37
|
+
import { internalError, invalidArgumentError } from "../errors.js";
|
|
38
|
+
|
|
39
|
+
/** The most entries one page returns, the contract's word on every paged request. */
|
|
40
|
+
export const LIST_PAGE_MAX_SIZE = 100;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The most rows one request examines for a page of `size`: five pages'
|
|
44
|
+
* worth, at least 100, at most 500. It bounds what one request asks of a
|
|
45
|
+
* composed read scope (on the hosted edition one authorization key per
|
|
46
|
+
* row, 50 per round trip) when the caller can see few of the rows.
|
|
47
|
+
*/
|
|
48
|
+
export function examineBudgetOf(size: number): number {
|
|
49
|
+
return Math.min(500, Math.max(100, size * 5));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The request fields every paged lane carries. */
|
|
53
|
+
export interface ListPageRequest {
|
|
54
|
+
readonly pageSize: number;
|
|
55
|
+
readonly pageToken: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ListPageParams<T, K extends string> {
|
|
59
|
+
readonly store: Store;
|
|
60
|
+
readonly declaration: ListIndexDeclaration<K>;
|
|
61
|
+
/** The request's own predicates, pushed into the store's indexed read. */
|
|
62
|
+
readonly query: Omit<ListIndexQuery<K>, "after" | "limit">;
|
|
63
|
+
readonly request: ListPageRequest;
|
|
64
|
+
/**
|
|
65
|
+
* The request's other fields, as text: a token is valid only for the
|
|
66
|
+
* request it was issued to.
|
|
67
|
+
*/
|
|
68
|
+
readonly fingerprint: string;
|
|
69
|
+
/** Decodes a row; undefined skips it (the lane logs, as it always has). */
|
|
70
|
+
readonly decode: (data: Uint8Array) => T | undefined;
|
|
71
|
+
/** The lane's per-row filter beyond the indexed predicates. */
|
|
72
|
+
readonly keep?: (row: T) => boolean;
|
|
73
|
+
/** The read scope, bound by the lane to its caller and kind. */
|
|
74
|
+
readonly scope: (rows: T[]) => Promise<T[]>;
|
|
75
|
+
/** The failure copy for a store fault. */
|
|
76
|
+
readonly failure: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ListPage<T> {
|
|
80
|
+
readonly entries: T[];
|
|
81
|
+
/** "" when the list is complete. */
|
|
82
|
+
readonly nextPageToken: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Reads one page (or, at `page_size` 0, the whole list) through the list index. */
|
|
86
|
+
export async function readListPage<T, K extends string>(
|
|
87
|
+
params: ListPageParams<T, K>,
|
|
88
|
+
): Promise<ListPage<T>> {
|
|
89
|
+
const { request } = params;
|
|
90
|
+
if (request.pageSize < 0) {
|
|
91
|
+
throw invalidArgumentError("page_size must not be negative");
|
|
92
|
+
}
|
|
93
|
+
let after =
|
|
94
|
+
request.pageToken === ""
|
|
95
|
+
? undefined
|
|
96
|
+
: decodeListPageToken(request.pageToken, params.fingerprint).cursor;
|
|
97
|
+
|
|
98
|
+
if (request.pageSize === 0) {
|
|
99
|
+
const rows = await queryOrFail(params, after, undefined);
|
|
100
|
+
return {
|
|
101
|
+
entries: await admit(params, rows).then((a) => a.map((e) => e.row)),
|
|
102
|
+
nextPageToken: "",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const size = Math.min(request.pageSize, LIST_PAGE_MAX_SIZE);
|
|
107
|
+
const budget = examineBudgetOf(size);
|
|
108
|
+
const kept: Array<Admitted<T>> = [];
|
|
109
|
+
let examined = 0;
|
|
110
|
+
for (;;) {
|
|
111
|
+
// One row past the batch is read only to learn whether more follow,
|
|
112
|
+
// so a list that ends exactly on a page carries no token (and no
|
|
113
|
+
// empty page behind it); the next batch reads that row again.
|
|
114
|
+
const batchSize = Math.min(size, budget - examined);
|
|
115
|
+
const read = await queryOrFail(params, after, batchSize + 1);
|
|
116
|
+
const more = read.length > batchSize;
|
|
117
|
+
const rows = more ? read.slice(0, batchSize) : read;
|
|
118
|
+
examined += rows.length;
|
|
119
|
+
kept.push(...(await admit(params, rows)));
|
|
120
|
+
const last = rows[rows.length - 1];
|
|
121
|
+
if (last !== undefined) {
|
|
122
|
+
after = last.cursor;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (kept.length >= size) {
|
|
126
|
+
const page = kept.slice(0, size);
|
|
127
|
+
const exhausted = !more && kept.length === size;
|
|
128
|
+
return {
|
|
129
|
+
entries: page.map((e) => e.row),
|
|
130
|
+
nextPageToken: exhausted
|
|
131
|
+
? ""
|
|
132
|
+
: encodeListPageToken(
|
|
133
|
+
page[page.length - 1]!.cursor,
|
|
134
|
+
params.fingerprint,
|
|
135
|
+
),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (!more) {
|
|
139
|
+
return { entries: kept.map((e) => e.row), nextPageToken: "" };
|
|
140
|
+
}
|
|
141
|
+
if (examined >= budget) {
|
|
142
|
+
return {
|
|
143
|
+
entries: kept.map((e) => e.row),
|
|
144
|
+
nextPageToken: encodeListPageToken(after!, params.fingerprint),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface Admitted<T> {
|
|
151
|
+
readonly row: T;
|
|
152
|
+
readonly cursor: ListIndexCursor;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function queryOrFail<T, K extends string>(
|
|
156
|
+
params: ListPageParams<T, K>,
|
|
157
|
+
after: ListIndexCursor | undefined,
|
|
158
|
+
limit: number | undefined,
|
|
159
|
+
): Promise<ListIndexRow[]> {
|
|
160
|
+
try {
|
|
161
|
+
return await params.store.queryResources(params.declaration, {
|
|
162
|
+
...params.query,
|
|
163
|
+
...(after === undefined ? {} : { after }),
|
|
164
|
+
...(limit === undefined ? {} : { limit }),
|
|
165
|
+
});
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw internalError(error, params.failure);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A batch through the lane's filter and the scope, each row keeping its cursor. */
|
|
172
|
+
async function admit<T, K extends string>(
|
|
173
|
+
params: ListPageParams<T, K>,
|
|
174
|
+
rows: ReadonlyArray<ListIndexRow>,
|
|
175
|
+
): Promise<Array<Admitted<T>>> {
|
|
176
|
+
const decoded: Array<Admitted<T>> = [];
|
|
177
|
+
for (const row of rows) {
|
|
178
|
+
const value = params.decode(row.data);
|
|
179
|
+
if (
|
|
180
|
+
value !== undefined &&
|
|
181
|
+
(params.keep === undefined || params.keep(value))
|
|
182
|
+
) {
|
|
183
|
+
decoded.push({ row: value, cursor: row.cursor });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (decoded.length === 0) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const allowed = new Set(await params.scope(decoded.map((d) => d.row)));
|
|
190
|
+
return decoded.filter((d) => allowed.has(d.row));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// =============================================================================
|
|
194
|
+
// The token
|
|
195
|
+
// =============================================================================
|
|
196
|
+
|
|
197
|
+
const TOKEN_VERSION = 1;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The position a token carries: the list index's cursor, plus, for a lane
|
|
201
|
+
* whose entries are finer than rows (the approvals of one execution), the
|
|
202
|
+
* entry's position within its row.
|
|
203
|
+
*/
|
|
204
|
+
export interface ListPageTokenCursor {
|
|
205
|
+
readonly cursor: ListIndexCursor;
|
|
206
|
+
readonly position?: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function fingerprintHash(fingerprint: string): string {
|
|
210
|
+
return createHash("sha256")
|
|
211
|
+
.update(fingerprint)
|
|
212
|
+
.digest("base64url")
|
|
213
|
+
.slice(0, 16);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** An opaque, versioned token for the position after `cursor`. */
|
|
217
|
+
export function encodeListPageToken(
|
|
218
|
+
cursor: ListIndexCursor,
|
|
219
|
+
fingerprint: string,
|
|
220
|
+
position?: number,
|
|
221
|
+
): string {
|
|
222
|
+
const body = {
|
|
223
|
+
v: TOKEN_VERSION,
|
|
224
|
+
c: cursor.createdAt,
|
|
225
|
+
i: cursor.id,
|
|
226
|
+
f: fingerprintHash(fingerprint),
|
|
227
|
+
...(position === undefined ? {} : { p: position }),
|
|
228
|
+
};
|
|
229
|
+
return Buffer.from(JSON.stringify(body)).toString("base64url");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The position a token carries, refused as `invalid page_token` (the copy
|
|
234
|
+
* every paged lane already answers with) when it is malformed, of a
|
|
235
|
+
* version this server never issued, or issued for another request.
|
|
236
|
+
*/
|
|
237
|
+
export function decodeListPageToken(
|
|
238
|
+
token: string,
|
|
239
|
+
fingerprint: string,
|
|
240
|
+
): ListPageTokenCursor {
|
|
241
|
+
let body: unknown;
|
|
242
|
+
try {
|
|
243
|
+
const bytes = Buffer.from(token, "base64url");
|
|
244
|
+
if (bytes.toString("base64url") !== token) {
|
|
245
|
+
throw new Error("not canonical base64url");
|
|
246
|
+
}
|
|
247
|
+
body = JSON.parse(bytes.toString("utf8"));
|
|
248
|
+
} catch {
|
|
249
|
+
throw invalidArgumentError("invalid page_token");
|
|
250
|
+
}
|
|
251
|
+
if (
|
|
252
|
+
typeof body !== "object" ||
|
|
253
|
+
body === null ||
|
|
254
|
+
(body as { v?: unknown }).v !== TOKEN_VERSION ||
|
|
255
|
+
typeof (body as { c?: unknown }).c !== "string" ||
|
|
256
|
+
typeof (body as { i?: unknown }).i !== "string" ||
|
|
257
|
+
typeof (body as { f?: unknown }).f !== "string"
|
|
258
|
+
) {
|
|
259
|
+
throw invalidArgumentError("invalid page_token");
|
|
260
|
+
}
|
|
261
|
+
const parsed = body as { c: string; i: string; f: string; p?: unknown };
|
|
262
|
+
if (parsed.f !== fingerprintHash(fingerprint)) {
|
|
263
|
+
throw invalidArgumentError("page_token was issued for a different request");
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
parsed.p !== undefined &&
|
|
267
|
+
!(Number.isSafeInteger(parsed.p) && (parsed.p as number) >= 0)
|
|
268
|
+
) {
|
|
269
|
+
throw invalidArgumentError("invalid page_token");
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
cursor: { createdAt: parsed.c, id: parsed.i },
|
|
273
|
+
...(parsed.p === undefined ? {} : { position: parsed.p as number }),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** A request's other fields as the text its token is bound to. */
|
|
278
|
+
export function listPageFingerprint(
|
|
279
|
+
fields: Readonly<Record<string, unknown>>,
|
|
280
|
+
): string {
|
|
281
|
+
return JSON.stringify(fields, (_key, value: unknown) =>
|
|
282
|
+
typeof value === "bigint" ? value.toString() : value,
|
|
283
|
+
);
|
|
284
|
+
}
|