@greatwait/contracts 0.1.0-alpha.2 → 0.1.0-alpha.4
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/README.md +11 -9
- package/dist/authority.d.ts +84 -21
- package/dist/authority.js +47 -2
- package/dist/billing.d.ts +1001 -0
- package/dist/billing.js +684 -0
- package/dist/callback-safety.d.ts +2 -2
- package/dist/callback-safety.js +2 -2
- package/dist/compatibility.d.ts +3 -3
- package/dist/compatibility.js +1 -1
- package/dist/data-export.d.ts +67 -0
- package/dist/data-export.js +182 -0
- package/dist/endpoints.d.ts +181 -240
- package/dist/endpoints.js +77 -81
- package/dist/events.d.ts +17 -17
- package/dist/events.js +1 -1
- package/dist/first-value-journey.d.ts +192 -42
- package/dist/first-value-journey.js +212 -6
- package/dist/forecast.d.ts +71 -71
- package/dist/forecast.js +1 -1
- package/dist/identifiers.d.ts +20 -20
- package/dist/identifiers.js +1 -1
- package/dist/identity.d.ts +45 -41
- package/dist/identity.js +5 -4
- package/dist/index.d.ts +13 -8
- package/dist/index.js +13 -8
- package/dist/mcp.d.ts +201 -0
- package/dist/mcp.js +214 -0
- package/dist/naming.d.ts +45 -0
- package/dist/naming.js +159 -0
- package/dist/oauth.d.ts +186 -172
- package/dist/oauth.js +84 -8
- package/dist/operation-definitions.d.ts +9 -0
- package/dist/operation-definitions.js +7 -0
- package/dist/operations.d.ts +4359 -2214
- package/dist/operations.js +2754 -881
- package/dist/organization-lifecycle.d.ts +34 -0
- package/dist/organization-lifecycle.js +122 -0
- package/dist/partition.d.ts +26 -0
- package/dist/policy.d.ts +4 -4
- package/dist/policy.js +9 -5
- package/dist/problems.d.ts +32 -26
- package/dist/problems.js +6 -2
- package/dist/protocol.d.ts +204 -75
- package/dist/protocol.js +108 -36
- package/dist/scheduling-records.d.ts +343 -285
- package/dist/scheduling-records.js +1 -1
- package/dist/scheduling.d.ts +670 -360
- package/dist/scheduling.js +382 -56
- package/dist/secrets.d.ts +119 -105
- package/dist/secrets.js +1 -1
- package/dist/signatures.d.ts +74 -74
- package/dist/signatures.js +1 -1
- package/dist/tenancy.d.ts +142 -68
- package/dist/tenancy.js +70 -17
- package/dist/test-sink.d.ts +127 -127
- package/dist/test-sink.js +1 -1
- package/dist/time.d.ts +4 -4
- package/dist/time.js +1 -1
- package/dist/timing.d.ts +65 -0
- package/dist/timing.js +82 -0
- package/dist/usage.d.ts +103 -47
- package/dist/usage.js +197 -31
- package/dist/workspace.d.ts +34 -34
- package/dist/workspace.js +1 -1
- package/dist/zod-values.d.ts +3 -0
- package/dist/zod-values.js +2 -0
- package/dist/zod.d.ts +2 -0
- package/dist/zod.js +2 -0
- package/generated/v1/examples/attempts.list.response.json +4 -0
- package/generated/v1/examples/attempts.read.response.json +4 -0
- package/generated/v1/examples/billing.autotopup.request.json +5 -0
- package/generated/v1/examples/billing.autotopup.response.json +120 -0
- package/generated/v1/examples/billing.autotopuppreview.request.json +19 -0
- package/generated/v1/examples/billing.autotopuppreview.response.json +45 -0
- package/generated/v1/examples/billing.budgets.request.json +13 -0
- package/generated/v1/examples/billing.budgets.response.json +22 -0
- package/generated/v1/examples/billing.ceiling.request.json +8 -0
- package/generated/v1/examples/billing.ceiling.response.json +120 -0
- package/generated/v1/examples/billing.change.request.json +5 -0
- package/generated/v1/examples/billing.change.response.json +12 -0
- package/generated/v1/examples/billing.funding.response.json +120 -0
- package/generated/v1/examples/billing.fundingquote.request.json +7 -0
- package/generated/v1/examples/billing.fundingquote.response.json +13 -0
- package/generated/v1/examples/billing.ledger.response.json +23 -0
- package/generated/v1/examples/billing.notices.response.json +41 -0
- package/generated/v1/examples/billing.notificationsettings.response.json +17 -0
- package/generated/v1/examples/billing.notificationsettingsupdate.request.json +9 -0
- package/generated/v1/examples/billing.notificationsettingsupdate.response.json +17 -0
- package/generated/v1/examples/billing.portal.response.json +3 -0
- package/generated/v1/examples/billing.projects.response.json +12 -0
- package/generated/v1/examples/billing.purchase.request.json +5 -0
- package/generated/v1/examples/billing.purchase.response.json +120 -0
- package/generated/v1/examples/billing.quote.request.json +4 -0
- package/generated/v1/examples/billing.quote.response.json +19 -0
- package/generated/v1/examples/billing.reconcile.response.json +73 -0
- package/generated/v1/examples/billing.refundrequest.request.json +10 -0
- package/generated/v1/examples/billing.refundrequest.response.json +11 -0
- package/generated/v1/examples/billing.refundresolution.request.json +4 -0
- package/generated/v1/examples/billing.refundresolution.response.json +120 -0
- package/generated/v1/examples/billing.summary.response.json +73 -0
- package/generated/v1/examples/context.read.response.json +6 -0
- package/generated/v1/examples/credentials.connection.response.json +16 -0
- package/generated/v1/examples/endpoints.acknowledgeSigningKeyRotation.response.json +18 -0
- package/generated/v1/examples/endpoints.create.response.json +1 -1
- package/generated/v1/examples/endpoints.disable.response.json +0 -1
- package/generated/v1/examples/{endpoints.verify.response.json → endpoints.enable.response.json} +4 -5
- package/generated/v1/examples/endpoints.list.response.json +1 -2
- package/generated/v1/examples/endpoints.read.response.json +2 -2
- package/generated/v1/examples/endpoints.revealSigningSecret.response.json +7 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.response.json +1 -2
- package/generated/v1/examples/endpoints.update.response.json +1 -1
- package/generated/v1/examples/executions.list.response.json +20 -0
- package/generated/v1/examples/exports.cancel.response.json +11 -0
- package/generated/v1/examples/exports.create.request.json +1 -0
- package/generated/v1/examples/exports.create.response.json +8 -0
- package/generated/v1/examples/exports.part.response.json +5 -0
- package/generated/v1/examples/exports.read.response.json +8 -0
- package/generated/v1/examples/occurrences.read.response.json +4 -0
- package/generated/v1/examples/organizations.close.request.json +5 -0
- package/generated/v1/examples/organizations.close.response.json +10 -0
- package/generated/v1/examples/organizations.lifecycle.response.json +10 -0
- package/generated/v1/examples/organizations.restore.request.json +3 -0
- package/generated/v1/examples/organizations.restore.response.json +10 -0
- package/generated/v1/examples/projects.archive.request.json +3 -0
- package/generated/v1/examples/projects.archive.response.json +14 -0
- package/generated/v1/examples/projects.create.response.json +10 -1
- package/generated/v1/examples/projects.delete.response.json +9 -0
- package/generated/v1/examples/projects.list.response.json +12 -2
- package/generated/v1/examples/projects.rename.request.json +3 -0
- package/generated/v1/examples/projects.rename.response.json +14 -0
- package/generated/v1/examples/schedules.archive.response.json +3 -0
- package/generated/v1/examples/schedules.cancel.response.json +3 -0
- package/generated/v1/examples/schedules.create.request.json +4 -0
- package/generated/v1/examples/schedules.create.response.json +3 -0
- package/generated/v1/examples/schedules.list.response.json +3 -0
- package/generated/v1/examples/schedules.pause.response.json +3 -0
- package/generated/v1/examples/schedules.read.response.json +6 -0
- package/generated/v1/examples/schedules.replace.request.json +4 -0
- package/generated/v1/examples/schedules.replace.response.json +3 -0
- package/generated/v1/examples/schedules.resume.response.json +3 -0
- package/generated/v1/examples/timing.summary.response.json +35 -0
- package/generated/v1/examples/usage.read.response.json +1 -0
- package/generated/v1/first-value-journey.json +19 -10
- package/generated/v1/manifest.json +1019 -53
- package/generated/v1/openapi.json +30597 -5709
- package/generated/v1/policy-matrix.json +290 -45
- package/generated/v1/schemas/attempts.list.response.json +24 -1
- package/generated/v1/schemas/attempts.read.response.json +24 -1
- package/generated/v1/schemas/billing.autotopup.request-headers.json +13 -0
- package/generated/v1/schemas/billing.autotopup.request.json +22 -0
- package/generated/v1/schemas/billing.autotopup.response-headers.json +10 -0
- package/generated/v1/schemas/billing.autotopup.response.json +552 -0
- package/generated/v1/schemas/billing.autotopuppreview.request.json +77 -0
- package/generated/v1/schemas/billing.autotopuppreview.response.json +277 -0
- package/generated/v1/schemas/billing.budgets.request-headers.json +13 -0
- package/generated/v1/schemas/billing.budgets.request.json +66 -0
- package/generated/v1/schemas/billing.budgets.response-headers.json +10 -0
- package/generated/v1/schemas/billing.budgets.response.json +92 -0
- package/generated/v1/schemas/billing.ceiling.request-headers.json +13 -0
- package/generated/v1/schemas/billing.ceiling.request.json +40 -0
- package/generated/v1/schemas/billing.ceiling.response-headers.json +10 -0
- package/generated/v1/schemas/billing.ceiling.response.json +552 -0
- package/generated/v1/schemas/billing.change.request-headers.json +13 -0
- package/generated/v1/schemas/billing.change.request.json +22 -0
- package/generated/v1/schemas/billing.change.response.json +119 -0
- package/generated/v1/schemas/billing.funding.response.json +552 -0
- package/generated/v1/schemas/billing.fundingquote.request.json +28 -0
- package/generated/v1/schemas/billing.fundingquote.response.json +61 -0
- package/generated/v1/schemas/billing.ledger.query.json +16 -0
- package/generated/v1/schemas/billing.ledger.response.json +125 -0
- package/generated/v1/schemas/billing.notices.query.json +16 -0
- package/generated/v1/schemas/billing.notices.response.json +278 -0
- package/generated/v1/schemas/billing.notificationsettings.response.json +70 -0
- package/generated/v1/schemas/billing.notificationsettingsupdate.request-headers.json +13 -0
- package/generated/v1/schemas/billing.notificationsettingsupdate.request.json +36 -0
- package/generated/v1/schemas/billing.notificationsettingsupdate.response-headers.json +10 -0
- package/generated/v1/schemas/billing.notificationsettingsupdate.response.json +70 -0
- package/generated/v1/schemas/billing.portal.response.json +11 -0
- package/generated/v1/schemas/billing.projects.response.json +119 -0
- package/generated/v1/schemas/billing.purchase.request-headers.json +13 -0
- package/generated/v1/schemas/billing.purchase.request.json +22 -0
- package/generated/v1/schemas/billing.purchase.response-headers.json +10 -0
- package/generated/v1/schemas/billing.purchase.response.json +552 -0
- package/generated/v1/schemas/billing.quote.request.json +20 -0
- package/generated/v1/schemas/billing.quote.response.json +86 -0
- package/generated/v1/schemas/billing.reconcile.response.json +468 -0
- package/generated/v1/schemas/billing.refundrequest.request-headers.json +13 -0
- package/generated/v1/schemas/billing.refundrequest.request.json +42 -0
- package/generated/v1/schemas/billing.refundrequest.response-headers.json +10 -0
- package/generated/v1/schemas/billing.refundrequest.response.json +45 -0
- package/generated/v1/schemas/billing.refundresolution.request-headers.json +13 -0
- package/generated/v1/schemas/billing.refundresolution.request.json +61 -0
- package/generated/v1/schemas/billing.refundresolution.response-headers.json +10 -0
- package/generated/v1/schemas/billing.refundresolution.response.json +552 -0
- package/generated/v1/schemas/billing.summary.response.json +468 -0
- package/generated/v1/schemas/context.read.response.json +45 -0
- package/generated/v1/schemas/credentials.connection.response.json +66 -0
- package/generated/v1/schemas/credentials.create.request.json +2 -0
- package/generated/v1/schemas/credentials.create.response.json +2 -0
- package/generated/v1/schemas/credentials.list.response.json +2 -0
- package/generated/v1/schemas/credentials.revoke.response.json +2 -0
- package/generated/v1/schemas/credentials.rotate.response.json +4 -0
- package/generated/v1/schemas/{endpoints.verify.request-headers.json → endpoints.acknowledgeSigningKeyRotation.request-headers.json} +2 -1
- package/generated/v1/schemas/{endpoints.verify.response.json → endpoints.acknowledgeSigningKeyRotation.response.json} +8 -21
- package/generated/v1/schemas/endpoints.create.request.json +5 -0
- package/generated/v1/schemas/endpoints.create.response.json +8 -21
- package/generated/v1/schemas/endpoints.disable.response.json +8 -21
- package/generated/v1/schemas/endpoints.enable.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.enable.response.json +87 -0
- package/generated/v1/schemas/endpoints.list.response.json +8 -21
- package/generated/v1/schemas/endpoints.read.response.json +8 -21
- package/generated/v1/schemas/endpoints.revealSigningSecret.response.json +43 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +8 -21
- package/generated/v1/schemas/endpoints.update.request.json +5 -0
- package/generated/v1/schemas/endpoints.update.response.json +8 -21
- package/generated/v1/schemas/executions.list.query.json +46 -0
- package/generated/v1/schemas/executions.list.response.json +116 -0
- package/generated/v1/schemas/exports.cancel.response.json +48 -0
- package/generated/v1/schemas/exports.create.request.json +6 -0
- package/generated/v1/schemas/exports.create.response.json +38 -0
- package/generated/v1/schemas/exports.part.query.json +11 -0
- package/generated/v1/schemas/exports.part.response.json +22 -0
- package/generated/v1/schemas/exports.read.response.json +38 -0
- package/generated/v1/schemas/grants.list.response.json +2 -0
- package/generated/v1/schemas/grants.revoke.response.json +2 -0
- package/generated/v1/schemas/occurrences.list.query.json +5 -0
- package/generated/v1/schemas/occurrences.list.response.json +23 -0
- package/generated/v1/schemas/occurrences.read.response.json +23 -0
- package/generated/v1/schemas/occurrences.redeliver.response.json +24 -1
- package/generated/v1/schemas/organizations.close.request.json +20 -0
- package/generated/v1/schemas/organizations.close.response.json +68 -0
- package/generated/v1/schemas/organizations.lifecycle.response.json +68 -0
- package/generated/v1/schemas/organizations.restore.request.json +13 -0
- package/generated/v1/schemas/organizations.restore.response.json +68 -0
- package/generated/v1/schemas/projects.archive.request-headers.json +11 -0
- package/generated/v1/schemas/projects.archive.request.json +10 -0
- package/generated/v1/schemas/projects.archive.response-headers.json +11 -0
- package/generated/v1/schemas/projects.archive.response.json +79 -0
- package/generated/v1/schemas/projects.create.response.json +55 -1
- package/generated/v1/schemas/projects.delete.request-headers.json +11 -0
- package/generated/v1/schemas/projects.delete.response.json +37 -0
- package/generated/v1/schemas/projects.list.response.json +61 -2
- package/generated/v1/schemas/projects.rename.request-headers.json +11 -0
- package/generated/v1/schemas/projects.rename.request.json +12 -0
- package/generated/v1/schemas/projects.rename.response-headers.json +11 -0
- package/generated/v1/schemas/projects.rename.response.json +79 -0
- package/generated/v1/schemas/schedules.archive.response.json +90 -2
- package/generated/v1/schemas/schedules.cancel.response.json +90 -2
- package/generated/v1/schemas/schedules.create.request.json +24 -3
- package/generated/v1/schemas/schedules.create.response.json +90 -2
- package/generated/v1/schemas/schedules.list.query.json +13 -0
- package/generated/v1/schemas/schedules.list.response.json +67 -2
- package/generated/v1/schemas/schedules.pause.response.json +90 -2
- package/generated/v1/schemas/schedules.read.response.json +90 -2
- package/generated/v1/schemas/schedules.replace.request.json +24 -3
- package/generated/v1/schemas/schedules.replace.response.json +90 -2
- package/generated/v1/schemas/schedules.resume.response.json +90 -2
- package/generated/v1/schemas/timing.summary.response.json +191 -0
- package/generated/v1/schemas/usage.read.query.json +7 -0
- package/generated/v1/schemas/usage.read.response.json +36 -1
- package/generated/v1/snippets/attempts.list.json +1 -0
- package/generated/v1/snippets/attempts.read.json +1 -0
- package/generated/v1/snippets/billing.autotopup.json +6 -0
- package/generated/v1/snippets/billing.autotopuppreview.json +6 -0
- package/generated/v1/snippets/billing.budgets.json +6 -0
- package/generated/v1/snippets/billing.ceiling.json +6 -0
- package/generated/v1/snippets/billing.change.json +6 -0
- package/generated/v1/snippets/billing.funding.json +6 -0
- package/generated/v1/snippets/billing.fundingquote.json +6 -0
- package/generated/v1/snippets/billing.ledger.json +6 -0
- package/generated/v1/snippets/billing.notices.json +6 -0
- package/generated/v1/snippets/billing.notificationsettings.json +6 -0
- package/generated/v1/snippets/billing.notificationsettingsupdate.json +6 -0
- package/generated/v1/snippets/billing.portal.json +6 -0
- package/generated/v1/snippets/billing.projects.json +6 -0
- package/generated/v1/snippets/billing.purchase.json +6 -0
- package/generated/v1/snippets/billing.quote.json +6 -0
- package/generated/v1/snippets/billing.reconcile.json +6 -0
- package/generated/v1/snippets/billing.refundrequest.json +6 -0
- package/generated/v1/snippets/billing.refundresolution.json +6 -0
- package/generated/v1/snippets/billing.summary.json +6 -0
- package/generated/v1/snippets/context.read.json +6 -0
- package/generated/v1/snippets/credentials.connection.json +6 -0
- package/generated/v1/snippets/credentials.create.json +1 -0
- package/generated/v1/snippets/credentials.list.json +1 -0
- package/generated/v1/snippets/credentials.revoke.json +1 -0
- package/generated/v1/snippets/credentials.rotate.json +1 -0
- package/generated/v1/snippets/endpoints.acknowledgeSigningKeyRotation.json +6 -0
- package/generated/v1/snippets/endpoints.create.json +1 -0
- package/generated/v1/snippets/endpoints.disable.json +1 -0
- package/generated/v1/snippets/{endpoints.verify.json → endpoints.enable.json} +4 -3
- package/generated/v1/snippets/endpoints.list.json +1 -0
- package/generated/v1/snippets/endpoints.read.json +1 -0
- package/generated/v1/snippets/endpoints.revealSigningSecret.json +6 -0
- package/generated/v1/snippets/endpoints.rotateSigningKey.json +1 -0
- package/generated/v1/snippets/endpoints.update.json +1 -0
- package/generated/v1/snippets/executions.list.json +6 -0
- package/generated/v1/snippets/exports.cancel.json +6 -0
- package/generated/v1/snippets/exports.create.json +6 -0
- package/generated/v1/snippets/exports.part.json +6 -0
- package/generated/v1/snippets/exports.read.json +6 -0
- package/generated/v1/snippets/forecast.read.json +1 -0
- package/generated/v1/snippets/forecast.windows.json +1 -0
- package/generated/v1/snippets/grants.list.json +1 -0
- package/generated/v1/snippets/grants.revoke.json +1 -0
- package/generated/v1/snippets/journey/await-occurrence.json +6 -0
- package/generated/v1/snippets/journey/check-credential.json +6 -0
- package/generated/v1/snippets/journey/check-service.json +6 -0
- package/generated/v1/snippets/journey/clean-up.json +6 -0
- package/generated/v1/snippets/journey/create-destination.json +6 -0
- package/generated/v1/snippets/journey/create-one-shot.json +6 -0
- package/generated/v1/snippets/journey/read-attempt.json +6 -0
- package/generated/v1/snippets/journey/read-schedule.json +6 -0
- package/generated/v1/snippets/journey/replay-one-shot.json +6 -0
- package/generated/v1/snippets/meta.scaffold.json +1 -0
- package/generated/v1/snippets/meta.version.json +1 -0
- package/generated/v1/snippets/occurrences.list.json +1 -0
- package/generated/v1/snippets/occurrences.read.json +1 -0
- package/generated/v1/snippets/occurrences.redeliver.json +1 -0
- package/generated/v1/snippets/organizations.close.json +6 -0
- package/generated/v1/snippets/organizations.lifecycle.json +6 -0
- package/generated/v1/snippets/organizations.restore.json +6 -0
- package/generated/v1/snippets/projects.archive.json +6 -0
- package/generated/v1/snippets/projects.create.json +1 -0
- package/generated/v1/snippets/projects.delete.json +6 -0
- package/generated/v1/snippets/projects.list.json +1 -0
- package/generated/v1/snippets/projects.rename.json +6 -0
- package/generated/v1/snippets/schedules.archive.json +1 -0
- package/generated/v1/snippets/schedules.cancel.json +1 -0
- package/generated/v1/snippets/schedules.create.json +4 -3
- package/generated/v1/snippets/schedules.list.json +1 -0
- package/generated/v1/snippets/schedules.pause.json +1 -0
- package/generated/v1/snippets/schedules.read.json +1 -0
- package/generated/v1/snippets/schedules.replace.json +4 -3
- package/generated/v1/snippets/schedules.resume.json +1 -0
- package/generated/v1/snippets/sinkdeliveries.list.json +1 -0
- package/generated/v1/snippets/sinkdeliveries.read.json +1 -0
- package/generated/v1/snippets/testsinks.configure.json +1 -0
- package/generated/v1/snippets/testsinks.create.json +1 -0
- package/generated/v1/snippets/testsinks.delete.json +1 -0
- package/generated/v1/snippets/testsinks.list.json +1 -0
- package/generated/v1/snippets/testsinks.read.json +1 -0
- package/generated/v1/snippets/timing.summary.json +6 -0
- package/generated/v1/snippets/usage.read.json +1 -0
- package/package.json +2 -7
- package/dist/callback-ownership.d.ts +0 -58
- package/dist/callback-ownership.js +0 -71
- package/generated/v1/callback-endpoint-verification.json +0 -59
- /package/generated/v1/schemas/{endpoints.verify.response-headers.json → endpoints.acknowledgeSigningKeyRotation.response-headers.json} +0 -0
package/dist/usage.d.ts
CHANGED
|
@@ -1,28 +1,65 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
1
|
+
import { z } from "./zod.js";
|
|
2
2
|
import type { FieldError } from "./problems.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* A legacy operational aggregate of terminal occurrences.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* five attempts are on the occurrence's own attempt history, which is the page
|
|
12
|
-
* that answers "what did it cost us".
|
|
6
|
+
* The published field names predate PAYG and remain for client compatibility.
|
|
7
|
+
* They are no longer the invoice meter: PAYG charges each logical delivery
|
|
8
|
+
* attempt, including retries and redeliveries, through the exact billing ledger.
|
|
9
|
+
* A schedule that failed four times and succeeded on the fifth appears here as
|
|
10
|
+
* one terminal occurrence and in `billing.ledger` as five distinct attempts.
|
|
13
11
|
*
|
|
14
12
|
* Days are UTC and half-open, aligned to midnight at both ends. A tenant-local
|
|
15
13
|
* boundary would make every bucket a different width for a different customer
|
|
16
14
|
* and could not be one index range; the capacity decision states the budget in
|
|
17
15
|
* UTC days for the same reason.
|
|
18
16
|
*
|
|
19
|
-
* A day with no
|
|
17
|
+
* A day with no terminal occurrence is **absent** rather than zero. The window
|
|
20
18
|
* says which days the answer covers, so an absent day is unambiguous, and a
|
|
21
19
|
* quiet month is a short document rather than thirty-one zeroes.
|
|
22
20
|
*/
|
|
23
21
|
export declare const USAGE_BOUNDS: {
|
|
22
|
+
/**
|
|
23
|
+
* The bucket a caller gets without asking: one UTC day.
|
|
24
|
+
*
|
|
25
|
+
* Unchanged from the only width this surface had, so a caller that never
|
|
26
|
+
* heard of `bucketSeconds` sees exactly what it saw before.
|
|
27
|
+
*/
|
|
28
|
+
readonly defaultBucketSeconds: 86400;
|
|
24
29
|
/** Days a window covers when the caller names neither end. */
|
|
25
30
|
readonly defaultWindowDays: 31;
|
|
31
|
+
/**
|
|
32
|
+
* Buckets one answer may carry.
|
|
33
|
+
*
|
|
34
|
+
* A year of hours is 8,784, which is not a chart and not a document anybody
|
|
35
|
+
* reads — it is a client asking for the widest window at the finest width and
|
|
36
|
+
* getting a megabyte back. The ceiling is a refusal rather than a truncation,
|
|
37
|
+
* for the reason the forecast gives: an answer that is quietly short is one a
|
|
38
|
+
* caller will act on.
|
|
39
|
+
*
|
|
40
|
+
* Matched to `FORECAST_BOUNDS.maxBuckets`, because past and future now answer
|
|
41
|
+
* the same question in the same shape and a client charting both should not
|
|
42
|
+
* discover that one of them stops earlier.
|
|
43
|
+
*/
|
|
44
|
+
readonly maxBuckets: 1500;
|
|
45
|
+
/**
|
|
46
|
+
* The widest bucket, which is also the default: a day.
|
|
47
|
+
*
|
|
48
|
+
* Wider than a day is a question about months, and months are not a whole
|
|
49
|
+
* number of days — a "monthly" bucket would have to pick a calendar and this
|
|
50
|
+
* surface is deliberately in UTC days for every tenant.
|
|
51
|
+
*/
|
|
52
|
+
readonly maxBucketSeconds: 86400;
|
|
53
|
+
/**
|
|
54
|
+
* The finest bucket, which is an hour.
|
|
55
|
+
*
|
|
56
|
+
* Finer than an hour is not refused for cost — the read is the same indexed
|
|
57
|
+
* range scan at any width, and the row count depends on the window rather
|
|
58
|
+
* than the bucket — but because nothing asks for it. The dashboard's finest
|
|
59
|
+
* chart is hourly, and a width nothing consumes is a width nobody has
|
|
60
|
+
* checked the arithmetic of.
|
|
61
|
+
*/
|
|
62
|
+
readonly minBucketSeconds: 3600;
|
|
26
63
|
/**
|
|
27
64
|
* The widest window this contract will describe.
|
|
28
65
|
*
|
|
@@ -32,19 +69,20 @@ export declare const USAGE_BOUNDS: {
|
|
|
32
69
|
*/
|
|
33
70
|
readonly maxWindowDays: 366;
|
|
34
71
|
};
|
|
35
|
-
/** How a
|
|
72
|
+
/** How a terminal occurrence ended. The four retained terminal states. */
|
|
36
73
|
export declare const BILLABLE_TERMINAL_STATES: readonly ["cancelled", "skipped_stale", "succeeded", "terminal_failed"];
|
|
37
|
-
export declare const billableTerminalStateSchema:
|
|
74
|
+
export declare const billableTerminalStateSchema: import("zod").ZodEnum<{
|
|
38
75
|
succeeded: "succeeded";
|
|
39
76
|
cancelled: "cancelled";
|
|
40
77
|
terminal_failed: "terminal_failed";
|
|
41
78
|
skipped_stale: "skipped_stale";
|
|
42
79
|
}>;
|
|
43
80
|
export type BillableTerminalStateName = z.infer<typeof billableTerminalStateSchema>;
|
|
44
|
-
export declare const usageRequestSchema:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
81
|
+
export declare const usageRequestSchema: import("zod").ZodObject<{
|
|
82
|
+
bucketSeconds: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
83
|
+
from: import("zod").ZodOptional<import("zod").ZodString>;
|
|
84
|
+
to: import("zod").ZodOptional<import("zod").ZodString>;
|
|
85
|
+
}, import("zod/v4/core").$strict>;
|
|
48
86
|
export type UsageRequest = z.infer<typeof usageRequestSchema>;
|
|
49
87
|
export type UsageRequestParse = {
|
|
50
88
|
readonly errors: readonly FieldError[];
|
|
@@ -65,43 +103,51 @@ export type UsageRequestParse = {
|
|
|
65
103
|
export declare function parseUsageRequest(query: URLSearchParams): UsageRequestParse;
|
|
66
104
|
/** What a caller is told when a usage window is refused rather than served. */
|
|
67
105
|
export declare const USAGE_WINDOW_REASONS: Readonly<Record<UsageWindowRefusal, string>>;
|
|
68
|
-
export declare const usageDaySchema:
|
|
69
|
-
billableOccurrences:
|
|
70
|
-
byTerminalState:
|
|
106
|
+
export declare const usageDaySchema: import("zod").ZodObject<{
|
|
107
|
+
billableOccurrences: import("zod").ZodNumber;
|
|
108
|
+
byTerminalState: import("zod").ZodRecord<import("zod").ZodEnum<{
|
|
71
109
|
succeeded: "succeeded";
|
|
72
110
|
cancelled: "cancelled";
|
|
73
111
|
terminal_failed: "terminal_failed";
|
|
74
112
|
skipped_stale: "skipped_stale";
|
|
75
|
-
}> &
|
|
76
|
-
start:
|
|
77
|
-
},
|
|
113
|
+
}> & import("zod/v4/core").$partial, import("zod").ZodNumber>;
|
|
114
|
+
start: import("zod").ZodString;
|
|
115
|
+
}, import("zod/v4/core").$strip>;
|
|
78
116
|
export type UsageDayView = z.infer<typeof usageDaySchema>;
|
|
79
|
-
export declare const usageSchema:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
117
|
+
export declare const usageSchema: import("zod").ZodObject<{
|
|
118
|
+
bucketSeconds: import("zod").ZodNumber;
|
|
119
|
+
days: import("zod").ZodArray<import("zod").ZodObject<{
|
|
120
|
+
billableOccurrences: import("zod").ZodNumber;
|
|
121
|
+
byTerminalState: import("zod").ZodRecord<import("zod").ZodEnum<{
|
|
83
122
|
succeeded: "succeeded";
|
|
84
123
|
cancelled: "cancelled";
|
|
85
124
|
terminal_failed: "terminal_failed";
|
|
86
125
|
skipped_stale: "skipped_stale";
|
|
87
|
-
}> &
|
|
88
|
-
start:
|
|
89
|
-
},
|
|
90
|
-
freshness:
|
|
91
|
-
kind:
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
},
|
|
126
|
+
}> & import("zod/v4/core").$partial, import("zod").ZodNumber>;
|
|
127
|
+
start: import("zod").ZodString;
|
|
128
|
+
}, import("zod/v4/core").$strip>>;
|
|
129
|
+
freshness: import("zod").ZodObject<{
|
|
130
|
+
kind: import("zod").ZodLiteral<"authoritative">;
|
|
131
|
+
}, import("zod/v4/core").$strip>;
|
|
132
|
+
plan: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
133
|
+
attemptsToday: import("zod").ZodNumber;
|
|
134
|
+
dailyAttemptCeiling: import("zod").ZodNumber;
|
|
135
|
+
resetsAt: import("zod").ZodString;
|
|
136
|
+
since: import("zod").ZodString;
|
|
137
|
+
}, import("zod/v4/core").$strip>>;
|
|
138
|
+
totalBillableOccurrences: import("zod").ZodNumber;
|
|
139
|
+
window: import("zod").ZodObject<{
|
|
140
|
+
from: import("zod").ZodString;
|
|
141
|
+
to: import("zod").ZodString;
|
|
142
|
+
}, import("zod/v4/core").$strip>;
|
|
143
|
+
}, import("zod/v4/core").$strip>;
|
|
99
144
|
export type UsageView = z.infer<typeof usageSchema>;
|
|
100
|
-
export type UsageWindowRefusal = "window-inverted" | "window-too-wide" | "window-unreadable";
|
|
145
|
+
export type UsageWindowRefusal = "too-many-buckets" | "window-inverted" | "window-too-wide" | "window-unreadable";
|
|
101
146
|
export type ResolvedUsageWindow = {
|
|
102
147
|
readonly ok: false;
|
|
103
148
|
readonly refusal: UsageWindowRefusal;
|
|
104
149
|
} | {
|
|
150
|
+
readonly bucketSeconds: number;
|
|
105
151
|
readonly fromMs: number;
|
|
106
152
|
readonly ok: true;
|
|
107
153
|
readonly toMs: number;
|
|
@@ -109,13 +155,23 @@ export type ResolvedUsageWindow = {
|
|
|
109
155
|
/** UTC midnight of the day an instant falls in. */
|
|
110
156
|
export declare function utcMidnightMs(instantMs: number): number;
|
|
111
157
|
/**
|
|
112
|
-
* Resolve the window a usage request names, aligned to
|
|
158
|
+
* Resolve the window a usage request names, aligned to whole buckets.
|
|
159
|
+
*
|
|
160
|
+
* **Both ends are snapped outward, and that is not tidiness.** A window
|
|
161
|
+
* beginning at nine in the morning would put a partial bucket at the front of
|
|
162
|
+
* the list with no way for a reader to tell it from a quiet one — a bar that is
|
|
163
|
+
* short because it is twenty-three minutes long rather than because nothing
|
|
164
|
+
* happened. Snapping outward means the answer always covers whole buckets and
|
|
165
|
+
* never covers less than what was asked for.
|
|
113
166
|
*
|
|
114
|
-
* **
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
167
|
+
* **To the bucket rather than to the day, since FXO-2281**, which is the same
|
|
168
|
+
* argument one width down. It is the same behaviour at the default width: a
|
|
169
|
+
* day-wide bucket aligned to the epoch starts at UTC midnight, so a caller who
|
|
170
|
+
* names no width gets exactly the window this always served. An hourly caller
|
|
171
|
+
* asking for "the last twenty-four hours" would otherwise have both ends pushed
|
|
172
|
+
* to midnight and be handed up to forty-eight buckets, half of them for a day
|
|
173
|
+
* they did not ask about.
|
|
120
174
|
*/
|
|
121
|
-
export declare function resolveUsageWindow(request: UsageRequest,
|
|
175
|
+
export declare function resolveUsageWindow(request: Omit<UsageRequest, "bucketSeconds"> & {
|
|
176
|
+
readonly bucketSeconds?: number;
|
|
177
|
+
}, nowMs: number): ResolvedUsageWindow;
|
package/dist/usage.js
CHANGED
|
@@ -1,28 +1,65 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
1
|
+
import { z } from "./zod.js";
|
|
2
2
|
import { timestampSchema } from "./time.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* A legacy operational aggregate of terminal occurrences.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* five attempts are on the occurrence's own attempt history, which is the page
|
|
12
|
-
* that answers "what did it cost us".
|
|
6
|
+
* The published field names predate PAYG and remain for client compatibility.
|
|
7
|
+
* They are no longer the invoice meter: PAYG charges each logical delivery
|
|
8
|
+
* attempt, including retries and redeliveries, through the exact billing ledger.
|
|
9
|
+
* A schedule that failed four times and succeeded on the fifth appears here as
|
|
10
|
+
* one terminal occurrence and in `billing.ledger` as five distinct attempts.
|
|
13
11
|
*
|
|
14
12
|
* Days are UTC and half-open, aligned to midnight at both ends. A tenant-local
|
|
15
13
|
* boundary would make every bucket a different width for a different customer
|
|
16
14
|
* and could not be one index range; the capacity decision states the budget in
|
|
17
15
|
* UTC days for the same reason.
|
|
18
16
|
*
|
|
19
|
-
* A day with no
|
|
17
|
+
* A day with no terminal occurrence is **absent** rather than zero. The window
|
|
20
18
|
* says which days the answer covers, so an absent day is unambiguous, and a
|
|
21
19
|
* quiet month is a short document rather than thirty-one zeroes.
|
|
22
20
|
*/
|
|
23
21
|
export const USAGE_BOUNDS = {
|
|
22
|
+
/**
|
|
23
|
+
* The bucket a caller gets without asking: one UTC day.
|
|
24
|
+
*
|
|
25
|
+
* Unchanged from the only width this surface had, so a caller that never
|
|
26
|
+
* heard of `bucketSeconds` sees exactly what it saw before.
|
|
27
|
+
*/
|
|
28
|
+
defaultBucketSeconds: 86_400,
|
|
24
29
|
/** Days a window covers when the caller names neither end. */
|
|
25
30
|
defaultWindowDays: 31,
|
|
31
|
+
/**
|
|
32
|
+
* Buckets one answer may carry.
|
|
33
|
+
*
|
|
34
|
+
* A year of hours is 8,784, which is not a chart and not a document anybody
|
|
35
|
+
* reads — it is a client asking for the widest window at the finest width and
|
|
36
|
+
* getting a megabyte back. The ceiling is a refusal rather than a truncation,
|
|
37
|
+
* for the reason the forecast gives: an answer that is quietly short is one a
|
|
38
|
+
* caller will act on.
|
|
39
|
+
*
|
|
40
|
+
* Matched to `FORECAST_BOUNDS.maxBuckets`, because past and future now answer
|
|
41
|
+
* the same question in the same shape and a client charting both should not
|
|
42
|
+
* discover that one of them stops earlier.
|
|
43
|
+
*/
|
|
44
|
+
maxBuckets: 1_500,
|
|
45
|
+
/**
|
|
46
|
+
* The widest bucket, which is also the default: a day.
|
|
47
|
+
*
|
|
48
|
+
* Wider than a day is a question about months, and months are not a whole
|
|
49
|
+
* number of days — a "monthly" bucket would have to pick a calendar and this
|
|
50
|
+
* surface is deliberately in UTC days for every tenant.
|
|
51
|
+
*/
|
|
52
|
+
maxBucketSeconds: 86_400,
|
|
53
|
+
/**
|
|
54
|
+
* The finest bucket, which is an hour.
|
|
55
|
+
*
|
|
56
|
+
* Finer than an hour is not refused for cost — the read is the same indexed
|
|
57
|
+
* range scan at any width, and the row count depends on the window rather
|
|
58
|
+
* than the bucket — but because nothing asks for it. The dashboard's finest
|
|
59
|
+
* chart is hourly, and a width nothing consumes is a width nobody has
|
|
60
|
+
* checked the arithmetic of.
|
|
61
|
+
*/
|
|
62
|
+
minBucketSeconds: 3_600,
|
|
26
63
|
/**
|
|
27
64
|
* The widest window this contract will describe.
|
|
28
65
|
*
|
|
@@ -32,7 +69,7 @@ export const USAGE_BOUNDS = {
|
|
|
32
69
|
*/
|
|
33
70
|
maxWindowDays: 366,
|
|
34
71
|
};
|
|
35
|
-
/** How a
|
|
72
|
+
/** How a terminal occurrence ended. The four retained terminal states. */
|
|
36
73
|
export const BILLABLE_TERMINAL_STATES = [
|
|
37
74
|
"cancelled",
|
|
38
75
|
"skipped_stale",
|
|
@@ -42,13 +79,42 @@ export const BILLABLE_TERMINAL_STATES = [
|
|
|
42
79
|
export const billableTerminalStateSchema = z.enum(BILLABLE_TERMINAL_STATES);
|
|
43
80
|
export const usageRequestSchema = z
|
|
44
81
|
.object({
|
|
82
|
+
/**
|
|
83
|
+
* Bucket width in whole seconds, aligned to the epoch.
|
|
84
|
+
*
|
|
85
|
+
* **Mirrors `/v1/forecast`'s parameter, deliberately** — past and future
|
|
86
|
+
* answer the same question about the same book, and a client charting both
|
|
87
|
+
* should not have to learn two spellings of "by the hour". Seconds as a
|
|
88
|
+
* number rather than a duration string, which is this product's published
|
|
89
|
+
* convention.
|
|
90
|
+
*
|
|
91
|
+
* A whole number of the finest bucket, so re-aggregating is exact: an
|
|
92
|
+
* answer whose buckets straddled would be a number nobody can reconcile
|
|
93
|
+
* against a bill.
|
|
94
|
+
*/
|
|
95
|
+
bucketSeconds: z
|
|
96
|
+
.number()
|
|
97
|
+
.int()
|
|
98
|
+
.min(USAGE_BOUNDS.minBucketSeconds)
|
|
99
|
+
.max(USAGE_BOUNDS.maxBucketSeconds)
|
|
100
|
+
.multipleOf(USAGE_BOUNDS.minBucketSeconds)
|
|
101
|
+
.default(USAGE_BOUNDS.defaultBucketSeconds),
|
|
45
102
|
/** Inclusive start. Defaults to `defaultWindowDays` before the end. */
|
|
46
103
|
from: timestampSchema.optional(),
|
|
47
104
|
/** Exclusive end. Defaults to the start of tomorrow, UTC. */
|
|
48
105
|
to: timestampSchema.optional(),
|
|
49
106
|
})
|
|
50
107
|
.strict();
|
|
51
|
-
|
|
108
|
+
/** `[name, numeric]` — the coercion each parameter needs off a URL. */
|
|
109
|
+
const USAGE_PARAMETERS = [
|
|
110
|
+
["bucketSeconds", true],
|
|
111
|
+
["from", false],
|
|
112
|
+
["to", false],
|
|
113
|
+
];
|
|
114
|
+
/** Query values arrive as strings; only a whole number may become one. */
|
|
115
|
+
function asNumberIfInteger(value) {
|
|
116
|
+
return /^-?\d+$/u.test(value) ? Number(value) : value;
|
|
117
|
+
}
|
|
52
118
|
/**
|
|
53
119
|
* Read a usage request off a URL.
|
|
54
120
|
*
|
|
@@ -61,7 +127,7 @@ const USAGE_PARAMETERS = ["from", "to"];
|
|
|
61
127
|
export function parseUsageRequest(query) {
|
|
62
128
|
const errors = [];
|
|
63
129
|
const raw = {};
|
|
64
|
-
for (const field of USAGE_PARAMETERS) {
|
|
130
|
+
for (const [field, numeric] of USAGE_PARAMETERS) {
|
|
65
131
|
const values = query.getAll(field);
|
|
66
132
|
if (values.length > 1) {
|
|
67
133
|
errors.push({
|
|
@@ -73,7 +139,7 @@ export function parseUsageRequest(query) {
|
|
|
73
139
|
const value = values[0];
|
|
74
140
|
if (value === undefined)
|
|
75
141
|
continue;
|
|
76
|
-
raw[field] = value;
|
|
142
|
+
raw[field] = numeric ? asNumberIfInteger(value) : value;
|
|
77
143
|
}
|
|
78
144
|
if (errors.length > 0)
|
|
79
145
|
return { errors, ok: false };
|
|
@@ -90,6 +156,7 @@ export function parseUsageRequest(query) {
|
|
|
90
156
|
}
|
|
91
157
|
/** What a caller is told when a usage window is refused rather than served. */
|
|
92
158
|
export const USAGE_WINDOW_REASONS = {
|
|
159
|
+
"too-many-buckets": `must not ask for more than ${String(USAGE_BOUNDS.maxBuckets)} buckets; widen bucketSeconds or narrow the window`,
|
|
93
160
|
"window-inverted": "must end after it begins",
|
|
94
161
|
"window-too-wide": `must cover at most ${String(USAGE_BOUNDS.maxWindowDays)} days`,
|
|
95
162
|
"window-unreadable": "must be an RFC 3339 instant",
|
|
@@ -97,7 +164,7 @@ export const USAGE_WINDOW_REASONS = {
|
|
|
97
164
|
export const usageDaySchema = z.object({
|
|
98
165
|
billableOccurrences: z.number().int().positive(),
|
|
99
166
|
/**
|
|
100
|
-
* How the day's
|
|
167
|
+
* How the day's terminal occurrences ended.
|
|
101
168
|
*
|
|
102
169
|
* Sums to `billableOccurrences`, and a state with no occurrences is absent
|
|
103
170
|
* rather than zero — the same rule the day list follows, one level down.
|
|
@@ -108,7 +175,22 @@ export const usageDaySchema = z.object({
|
|
|
108
175
|
});
|
|
109
176
|
export const usageSchema = z
|
|
110
177
|
.object({
|
|
111
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* The width each entry in `days` covers, in seconds.
|
|
180
|
+
*
|
|
181
|
+
* Echoed so a client knows what it got rather than what it asked for, the
|
|
182
|
+
* way the forecast echoes its own. It is `86400` for every caller that does
|
|
183
|
+
* not ask, which is what this surface always served.
|
|
184
|
+
*/
|
|
185
|
+
bucketSeconds: z.number().int().positive(),
|
|
186
|
+
/**
|
|
187
|
+
* Ascending by `start`. Buckets with no terminal occurrence are absent.
|
|
188
|
+
*
|
|
189
|
+
* **Still named `days`, at a width that may not be one.** Renaming it would
|
|
190
|
+
* break every caller reading the field to serve callers who asked for an
|
|
191
|
+
* hourly width — which is nobody until they opt in. `bucketSeconds` above
|
|
192
|
+
* says what a row spans.
|
|
193
|
+
*/
|
|
112
194
|
days: z.array(usageDaySchema),
|
|
113
195
|
/**
|
|
114
196
|
* Authoritative, because this is the committed model the settlement wrote.
|
|
@@ -119,6 +201,44 @@ export const usageSchema = z
|
|
|
119
201
|
* summary.
|
|
120
202
|
*/
|
|
121
203
|
freshness: z.object({ kind: z.literal("authoritative") }),
|
|
204
|
+
/**
|
|
205
|
+
* How much of this deployment's daily attempt allowance has been spent.
|
|
206
|
+
*
|
|
207
|
+
* **Because nothing else could say it, and something had to.** The runtime
|
|
208
|
+
* refuses admission once an organization has made
|
|
209
|
+
* `dailyAttemptCeiling` attempts in a UTC day, and defers every schedule to
|
|
210
|
+
* the next day boundary rather than failing them. Nothing lost, nothing
|
|
211
|
+
* failed — and nothing running either, for up to a day, on a product whose
|
|
212
|
+
* whole job is to run things. The operator found his own account in that
|
|
213
|
+
* state during the W4-10 walk and no screen anywhere named it: the runs
|
|
214
|
+
* read "Delivering", the dashboard had no action waiting, and the only
|
|
215
|
+
* visible trace was an unremarkable number on the billing panel.
|
|
216
|
+
*
|
|
217
|
+
* The disposition, the ceiling and the count all lived inside
|
|
218
|
+
* `DialerWorker` and `EntitlementPolicy`; neither ever left
|
|
219
|
+
* `packages/application`, and `days` above counts settled *occurrences*
|
|
220
|
+
* rather than attempts, so it could not be derived either.
|
|
221
|
+
*
|
|
222
|
+
* Absent when this deployment sets no ceiling, which is the honest shape
|
|
223
|
+
* for "there is no allowance to be near the end of" — a surface must not
|
|
224
|
+
* render a limit nobody has.
|
|
225
|
+
*/
|
|
226
|
+
plan: z
|
|
227
|
+
.object({
|
|
228
|
+
/** Attempts this organization has made since `since`, capped at one past the ceiling. */
|
|
229
|
+
attemptsToday: z.number().int().nonnegative(),
|
|
230
|
+
dailyAttemptCeiling: z.number().int().positive(),
|
|
231
|
+
/**
|
|
232
|
+
* When the allowance refills — the next UTC day boundary.
|
|
233
|
+
*
|
|
234
|
+
* The same instant the runtime defers to, so a page saying "runs
|
|
235
|
+
* resume at" and a row's own wake cannot disagree.
|
|
236
|
+
*/
|
|
237
|
+
resetsAt: timestampSchema,
|
|
238
|
+
/** The start of the day being counted, so a reader can check the window. */
|
|
239
|
+
since: timestampSchema,
|
|
240
|
+
})
|
|
241
|
+
.optional(),
|
|
122
242
|
totalBillableOccurrences: z.number().int().nonnegative(),
|
|
123
243
|
window: z.object({ from: timestampSchema, to: timestampSchema }),
|
|
124
244
|
})
|
|
@@ -138,7 +258,7 @@ export const usageSchema = z
|
|
|
138
258
|
path: ["totalBillableOccurrences"],
|
|
139
259
|
})
|
|
140
260
|
.refine((usage) => usage.days.every((day) => Object.values(day.byTerminalState).reduce((sum, count) => sum + count, 0) === day.billableOccurrences), {
|
|
141
|
-
message: "byTerminalState must account for every
|
|
261
|
+
message: "byTerminalState must account for every terminal occurrence",
|
|
142
262
|
path: ["days"],
|
|
143
263
|
})
|
|
144
264
|
.refine((usage) => usage.days.every((day) => {
|
|
@@ -166,35 +286,81 @@ export function utcMidnightMs(instantMs) {
|
|
|
166
286
|
return Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate());
|
|
167
287
|
}
|
|
168
288
|
/**
|
|
169
|
-
* Resolve the window a usage request names, aligned to
|
|
289
|
+
* Resolve the window a usage request names, aligned to whole buckets.
|
|
290
|
+
*
|
|
291
|
+
* **Both ends are snapped outward, and that is not tidiness.** A window
|
|
292
|
+
* beginning at nine in the morning would put a partial bucket at the front of
|
|
293
|
+
* the list with no way for a reader to tell it from a quiet one — a bar that is
|
|
294
|
+
* short because it is twenty-three minutes long rather than because nothing
|
|
295
|
+
* happened. Snapping outward means the answer always covers whole buckets and
|
|
296
|
+
* never covers less than what was asked for.
|
|
170
297
|
*
|
|
171
|
-
* **
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
298
|
+
* **To the bucket rather than to the day, since FXO-2281**, which is the same
|
|
299
|
+
* argument one width down. It is the same behaviour at the default width: a
|
|
300
|
+
* day-wide bucket aligned to the epoch starts at UTC midnight, so a caller who
|
|
301
|
+
* names no width gets exactly the window this always served. An hourly caller
|
|
302
|
+
* asking for "the last twenty-four hours" would otherwise have both ends pushed
|
|
303
|
+
* to midnight and be handed up to forty-eight buckets, half of them for a day
|
|
304
|
+
* they did not ask about.
|
|
177
305
|
*/
|
|
178
|
-
export function resolveUsageWindow(
|
|
179
|
-
|
|
306
|
+
export function resolveUsageWindow(
|
|
307
|
+
// **The request as a caller may hand it over, not only as the schema hands
|
|
308
|
+
// it back.** `bucketSeconds` carries a `.default()`, which makes it required
|
|
309
|
+
// on the parsed type; a caller resolving a window from a literal `{ from, to }`
|
|
310
|
+
// — which every case here and the contract mock do — is asking the same
|
|
311
|
+
// question and gets the same default from the `??` below.
|
|
312
|
+
request, nowMs) {
|
|
313
|
+
const bucketSeconds = request.bucketSeconds ?? USAGE_BOUNDS.defaultBucketSeconds;
|
|
314
|
+
const bucketMs = bucketSeconds * 1_000;
|
|
315
|
+
/** The start of the bucket an instant falls in, aligned to the epoch. */
|
|
316
|
+
const floorToBucket = (instantMs) => Math.floor(instantMs / bucketMs) * bucketMs;
|
|
317
|
+
/**
|
|
318
|
+
* The end of the bucket an instant falls in — and the instant itself when it
|
|
319
|
+
* is already a boundary.
|
|
320
|
+
*
|
|
321
|
+
* **The `+ bucketMs` here used to be unconditional**, which handed every
|
|
322
|
+
* caller asking a tidy question one bucket more than it asked for. A
|
|
323
|
+
* dashboard requesting seven whole days ending at midnight was served eight,
|
|
324
|
+
* and printed the served window under a heading that said seven. Windows
|
|
325
|
+
* here are half-open, so an aligned `to` is already exact; only an instant
|
|
326
|
+
* *inside* a bucket has anything to round up to.
|
|
327
|
+
*
|
|
328
|
+
* `defaultToMs` reads the same way and wants to: with `to` omitted the
|
|
329
|
+
* answer should reach the bucket in progress, and when `now` happens to land
|
|
330
|
+
* exactly on a boundary the bucket in progress is empty and belongs outside
|
|
331
|
+
* the window rather than on the end of it as a zero.
|
|
332
|
+
*/
|
|
333
|
+
const ceilToBucket = (instantMs) => {
|
|
334
|
+
const floored = floorToBucket(instantMs);
|
|
335
|
+
return floored === instantMs ? instantMs : floored + bucketMs;
|
|
336
|
+
};
|
|
337
|
+
const defaultToMs = ceilToBucket(nowMs);
|
|
180
338
|
const requestedToMs = request.to === undefined ? defaultToMs : Date.parse(request.to);
|
|
181
339
|
if (!Number.isFinite(requestedToMs)) {
|
|
182
340
|
return { ok: false, refusal: "window-unreadable" };
|
|
183
341
|
}
|
|
184
|
-
const toMs = request.to === undefined
|
|
185
|
-
? defaultToMs
|
|
186
|
-
: utcMidnightMs(requestedToMs) + DAY_MS;
|
|
342
|
+
const toMs = request.to === undefined ? defaultToMs : ceilToBucket(requestedToMs);
|
|
187
343
|
const requestedFromMs = request.from === undefined
|
|
188
344
|
? toMs - USAGE_BOUNDS.defaultWindowDays * DAY_MS
|
|
189
345
|
: Date.parse(request.from);
|
|
190
346
|
if (!Number.isFinite(requestedFromMs)) {
|
|
191
347
|
return { ok: false, refusal: "window-unreadable" };
|
|
192
348
|
}
|
|
193
|
-
const fromMs =
|
|
349
|
+
const fromMs = floorToBucket(requestedFromMs);
|
|
194
350
|
if (fromMs >= toMs)
|
|
195
351
|
return { ok: false, refusal: "window-inverted" };
|
|
196
352
|
if (toMs - fromMs > USAGE_BOUNDS.maxWindowDays * DAY_MS) {
|
|
197
353
|
return { ok: false, refusal: "window-too-wide" };
|
|
198
354
|
}
|
|
199
|
-
|
|
355
|
+
// **The bucket count, not the read.** The read is one indexed range scan over
|
|
356
|
+
// `occurrence_usage` at any width — `terminal_state` is an INCLUDE column on
|
|
357
|
+
// the index it uses, so the whole answer comes from the index and the rows it
|
|
358
|
+
// touches depend on the window rather than on how they are grouped. What
|
|
359
|
+
// grows with a finer bucket is the *answer*, and a year of hours is 8,784
|
|
360
|
+
// rows nobody charts. Refused rather than truncated: a quietly short answer
|
|
361
|
+
// is one a caller acts on.
|
|
362
|
+
if ((toMs - fromMs) / bucketMs > USAGE_BOUNDS.maxBuckets) {
|
|
363
|
+
return { ok: false, refusal: "too-many-buckets" };
|
|
364
|
+
}
|
|
365
|
+
return { bucketSeconds, fromMs, ok: true, toMs };
|
|
200
366
|
}
|