@raybase/core 0.0.2 → 0.3.3
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/LICENSE +21 -0
- package/dist/.admin-build/AdminApp.css +2 -0
- package/dist/.admin-build/AdminApp.js +77 -0
- package/dist/.admin-docs-build/AdminDocsApp.css +2 -0
- package/dist/.admin-docs-build/AdminDocsApp.js +45 -0
- package/dist/actions.d.ts +347 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +661 -0
- package/dist/actions.js.map +1 -0
- package/dist/admin-assets/admin.7738f50720a8ed7b.css +2 -0
- package/dist/admin-assets/admin.8678cb61db34dc81.js +77 -0
- package/dist/admin-boot.d.ts +94 -0
- package/dist/admin-boot.d.ts.map +1 -0
- package/dist/admin-boot.js +139 -0
- package/dist/admin-boot.js.map +1 -0
- package/dist/admin-docs-content.d.ts +95 -0
- package/dist/admin-docs-content.d.ts.map +1 -0
- package/dist/admin-docs-content.js +463 -0
- package/dist/admin-docs-content.js.map +1 -0
- package/dist/admin-docs-ui.d.ts +11 -0
- package/dist/admin-docs-ui.d.ts.map +1 -0
- package/dist/admin-docs-ui.js +23 -0
- package/dist/admin-docs-ui.js.map +1 -0
- package/dist/admin-kit.d.ts +29 -0
- package/dist/admin-kit.js +1557 -0
- package/dist/admin-role.d.ts +8 -0
- package/dist/admin-role.d.ts.map +1 -0
- package/dist/admin-role.js +22 -0
- package/dist/admin-role.js.map +1 -0
- package/dist/admin-ui.d.ts +3 -0
- package/dist/admin-ui.d.ts.map +1 -0
- package/dist/admin-ui.js +16 -0
- package/dist/admin-ui.js.map +1 -0
- package/dist/agent-ai-chat.d.ts +127 -0
- package/dist/agent-ai-chat.d.ts.map +1 -0
- package/dist/agent-ai-chat.js +5975 -0
- package/dist/agent-ai-chat.js.map +1 -0
- package/dist/agent-chapter-archive.d.ts +214 -0
- package/dist/agent-chapter-archive.d.ts.map +1 -0
- package/dist/agent-chapter-archive.js +696 -0
- package/dist/agent-chapter-archive.js.map +1 -0
- package/dist/agent-chapter-search.d.ts +116 -0
- package/dist/agent-chapter-search.d.ts.map +1 -0
- package/dist/agent-chapter-search.js +605 -0
- package/dist/agent-chapter-search.js.map +1 -0
- package/dist/agent-chapter-tools.d.ts +45 -0
- package/dist/agent-chapter-tools.d.ts.map +1 -0
- package/dist/agent-chapter-tools.js +344 -0
- package/dist/agent-chapter-tools.js.map +1 -0
- package/dist/agent-chapters.d.ts +228 -0
- package/dist/agent-chapters.d.ts.map +1 -0
- package/dist/agent-chapters.js +737 -0
- package/dist/agent-chapters.js.map +1 -0
- package/dist/agent-data-parts.d.ts +167 -0
- package/dist/agent-data-parts.d.ts.map +1 -0
- package/dist/agent-data-parts.js +68 -0
- package/dist/agent-data-parts.js.map +1 -0
- package/dist/agent-errors.d.ts +41 -0
- package/dist/agent-errors.d.ts.map +1 -0
- package/dist/agent-errors.js +191 -0
- package/dist/agent-errors.js.map +1 -0
- package/dist/agent-follow-up-store.d.ts +273 -0
- package/dist/agent-follow-up-store.d.ts.map +1 -0
- package/dist/agent-follow-up-store.js +703 -0
- package/dist/agent-follow-up-store.js.map +1 -0
- package/dist/agent-follow-ups.d.ts +314 -0
- package/dist/agent-follow-ups.d.ts.map +1 -0
- package/dist/agent-follow-ups.js +840 -0
- package/dist/agent-follow-ups.js.map +1 -0
- package/dist/agent-memory.d.ts +323 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +1270 -0
- package/dist/agent-memory.js.map +1 -0
- package/dist/agent-message-parts.d.ts +58 -0
- package/dist/agent-message-parts.d.ts.map +1 -0
- package/dist/agent-message-parts.js +163 -0
- package/dist/agent-message-parts.js.map +1 -0
- package/dist/agent-remote-mcp-credentials.d.ts +154 -0
- package/dist/agent-remote-mcp-credentials.d.ts.map +1 -0
- package/dist/agent-remote-mcp-credentials.js +287 -0
- package/dist/agent-remote-mcp-credentials.js.map +1 -0
- package/dist/agent-remote-mcp-crypto.d.ts +32 -0
- package/dist/agent-remote-mcp-crypto.d.ts.map +1 -0
- package/dist/agent-remote-mcp-crypto.js +101 -0
- package/dist/agent-remote-mcp-crypto.js.map +1 -0
- package/dist/agent-remote-mcp-install.d.ts +299 -0
- package/dist/agent-remote-mcp-install.d.ts.map +1 -0
- package/dist/agent-remote-mcp-install.js +785 -0
- package/dist/agent-remote-mcp-install.js.map +1 -0
- package/dist/agent-remote-mcp-oauth.d.ts +73 -0
- package/dist/agent-remote-mcp-oauth.d.ts.map +1 -0
- package/dist/agent-remote-mcp-oauth.js +344 -0
- package/dist/agent-remote-mcp-oauth.js.map +1 -0
- package/dist/agent-remote-mcp-transport.d.ts +46 -0
- package/dist/agent-remote-mcp-transport.d.ts.map +1 -0
- package/dist/agent-remote-mcp-transport.js +225 -0
- package/dist/agent-remote-mcp-transport.js.map +1 -0
- package/dist/agent-remote-mcp.d.ts +147 -0
- package/dist/agent-remote-mcp.d.ts.map +1 -0
- package/dist/agent-remote-mcp.js +681 -0
- package/dist/agent-remote-mcp.js.map +1 -0
- package/dist/agent-room-redaction.d.ts +66 -0
- package/dist/agent-room-redaction.d.ts.map +1 -0
- package/dist/agent-room-redaction.js +264 -0
- package/dist/agent-room-redaction.js.map +1 -0
- package/dist/agent-rooms.d.ts +209 -0
- package/dist/agent-rooms.d.ts.map +1 -0
- package/dist/agent-rooms.js +244 -0
- package/dist/agent-rooms.js.map +1 -0
- package/dist/agent-runtime.d.ts +541 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +4260 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/agent-skill-archive.d.ts +20 -0
- package/dist/agent-skill-archive.d.ts.map +1 -0
- package/dist/agent-skill-archive.js +316 -0
- package/dist/agent-skill-archive.js.map +1 -0
- package/dist/agent-skill-discovery.d.ts +96 -0
- package/dist/agent-skill-discovery.d.ts.map +1 -0
- package/dist/agent-skill-discovery.js +464 -0
- package/dist/agent-skill-discovery.js.map +1 -0
- package/dist/agent-skill-format.d.ts +57 -0
- package/dist/agent-skill-format.d.ts.map +1 -0
- package/dist/agent-skill-format.js +238 -0
- package/dist/agent-skill-format.js.map +1 -0
- package/dist/agent-skill-import.d.ts +64 -0
- package/dist/agent-skill-import.d.ts.map +1 -0
- package/dist/agent-skill-import.js +295 -0
- package/dist/agent-skill-import.js.map +1 -0
- package/dist/agent-skill-publish.d.ts +51 -0
- package/dist/agent-skill-publish.d.ts.map +1 -0
- package/dist/agent-skill-publish.js +136 -0
- package/dist/agent-skill-publish.js.map +1 -0
- package/dist/agent-skill-store.d.ts +252 -0
- package/dist/agent-skill-store.d.ts.map +1 -0
- package/dist/agent-skill-store.js +514 -0
- package/dist/agent-skill-store.js.map +1 -0
- package/dist/agent-skills.d.ts +85 -0
- package/dist/agent-skills.d.ts.map +1 -0
- package/dist/agent-skills.js +291 -0
- package/dist/agent-skills.js.map +1 -0
- package/dist/agent-transcript-pager.d.ts +77 -0
- package/dist/agent-transcript-pager.d.ts.map +1 -0
- package/dist/agent-transcript-pager.js +210 -0
- package/dist/agent-transcript-pager.js.map +1 -0
- package/dist/agent-user-data-parts.d.ts +20 -0
- package/dist/agent-user-data-parts.d.ts.map +1 -0
- package/dist/agent-user-data-parts.js +161 -0
- package/dist/agent-user-data-parts.js.map +1 -0
- package/dist/agent-user-transcript.d.ts +46 -0
- package/dist/agent-user-transcript.d.ts.map +1 -0
- package/dist/agent-user-transcript.js +106 -0
- package/dist/agent-user-transcript.js.map +1 -0
- package/dist/agent-voice-notes.d.ts +15 -0
- package/dist/agent-voice-notes.d.ts.map +1 -0
- package/dist/agent-voice-notes.js +17 -0
- package/dist/agent-voice-notes.js.map +1 -0
- package/dist/agent-wakes.d.ts +213 -0
- package/dist/agent-wakes.d.ts.map +1 -0
- package/dist/agent-wakes.js +534 -0
- package/dist/agent-wakes.js.map +1 -0
- package/dist/agent.d.ts +1780 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +3123 -0
- package/dist/agent.js.map +1 -0
- package/dist/ai.d.ts +583 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1858 -0
- package/dist/ai.js.map +1 -0
- package/dist/app-manifest.d.ts +255 -0
- package/dist/app-manifest.d.ts.map +1 -0
- package/dist/app-manifest.js +372 -0
- package/dist/app-manifest.js.map +1 -0
- package/dist/app-users.d.ts +30 -0
- package/dist/app-users.d.ts.map +1 -0
- package/dist/app-users.js +91 -0
- package/dist/app-users.js.map +1 -0
- package/dist/audit-archive.d.ts +85 -0
- package/dist/audit-archive.d.ts.map +1 -0
- package/dist/audit-archive.js +178 -0
- package/dist/audit-archive.js.map +1 -0
- package/dist/audit.d.ts +54 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +95 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth-logger.d.ts +28 -0
- package/dist/auth-logger.d.ts.map +1 -0
- package/dist/auth-logger.js +53 -0
- package/dist/auth-logger.js.map +1 -0
- package/dist/auth-password-secret.d.ts +59 -0
- package/dist/auth-password-secret.d.ts.map +1 -0
- package/dist/auth-password-secret.js +409 -0
- package/dist/auth-password-secret.js.map +1 -0
- package/dist/auth-schema.d.ts +120 -0
- package/dist/auth-schema.d.ts.map +1 -0
- package/dist/auth-schema.js +285 -0
- package/dist/auth-schema.js.map +1 -0
- package/dist/auth.d.ts +321 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +1562 -0
- package/dist/auth.js.map +1 -0
- package/dist/backup-archive.d.ts +257 -0
- package/dist/backup-archive.d.ts.map +1 -0
- package/dist/backup-archive.js +2068 -0
- package/dist/backup-archive.js.map +1 -0
- package/dist/backup-crypto.d.ts +313 -0
- package/dist/backup-crypto.d.ts.map +1 -0
- package/dist/backup-crypto.js +693 -0
- package/dist/backup-crypto.js.map +1 -0
- package/dist/backup-format.d.ts +200 -0
- package/dist/backup-format.d.ts.map +1 -0
- package/dist/backup-format.js +1247 -0
- package/dist/backup-format.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/clock.d.ts +35 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +83 -0
- package/dist/clock.js.map +1 -0
- package/dist/data-client.d.ts +187 -0
- package/dist/data-client.d.ts.map +1 -0
- package/dist/data-client.js +590 -0
- package/dist/data-client.js.map +1 -0
- package/dist/data-secrets.d.ts +164 -0
- package/dist/data-secrets.d.ts.map +1 -0
- package/dist/data-secrets.js +383 -0
- package/dist/data-secrets.js.map +1 -0
- package/dist/db.d.ts +453 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +324 -0
- package/dist/db.js.map +1 -0
- package/dist/dev-activity.d.ts +48 -0
- package/dist/dev-activity.d.ts.map +1 -0
- package/dist/dev-activity.js +50 -0
- package/dist/dev-activity.js.map +1 -0
- package/dist/do.d.ts +429 -0
- package/dist/do.d.ts.map +1 -0
- package/dist/do.js +4526 -0
- package/dist/do.js.map +1 -0
- package/dist/email.d.ts +137 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +527 -0
- package/dist/email.js.map +1 -0
- package/dist/generated/admin-assets.d.ts +16 -0
- package/dist/generated/admin-assets.d.ts.map +1 -0
- package/dist/generated/admin-assets.js +16 -0
- package/dist/generated/admin-assets.js.map +1 -0
- package/dist/generated/admin-docs-assets.d.ts +3 -0
- package/dist/generated/admin-docs-assets.d.ts.map +1 -0
- package/dist/generated/admin-docs-assets.js +3 -0
- package/dist/generated/admin-docs-assets.js.map +1 -0
- package/dist/handoff-consumer.d.ts +79 -0
- package/dist/handoff-consumer.d.ts.map +1 -0
- package/dist/handoff-consumer.js +300 -0
- package/dist/handoff-consumer.js.map +1 -0
- package/dist/handoff.d.ts +70 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +228 -0
- package/dist/handoff.js.map +1 -0
- package/dist/host-auth.d.ts +47 -0
- package/dist/host-auth.d.ts.map +1 -0
- package/dist/host-auth.js +59 -0
- package/dist/host-auth.js.map +1 -0
- package/dist/host-user.d.ts +15 -0
- package/dist/host-user.d.ts.map +1 -0
- package/dist/host-user.js +23 -0
- package/dist/host-user.js.map +1 -0
- package/dist/html.d.ts +35 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +634 -0
- package/dist/html.js.map +1 -0
- package/dist/http-error.d.ts +13 -0
- package/dist/http-error.d.ts.map +1 -0
- package/dist/http-error.js +15 -0
- package/dist/http-error.js.map +1 -0
- package/dist/imports.d.ts +219 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +571 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/inference-tape.d.ts +64 -0
- package/dist/inference-tape.d.ts.map +1 -0
- package/dist/inference-tape.js +278 -0
- package/dist/inference-tape.js.map +1 -0
- package/dist/internal.d.ts +15 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +48 -0
- package/dist/internal.js.map +1 -0
- package/dist/jobs.d.ts +796 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +538 -0
- package/dist/jobs.js.map +1 -0
- package/dist/json-schema.d.ts +91 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +884 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/knowledge.d.ts +33 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +348 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/local-dev-chrome.d.ts +29 -0
- package/dist/local-dev-chrome.d.ts.map +1 -0
- package/dist/local-dev-chrome.js +514 -0
- package/dist/local-dev-chrome.js.map +1 -0
- package/dist/local-dev-personas.d.ts +15 -0
- package/dist/local-dev-personas.d.ts.map +1 -0
- package/dist/local-dev-personas.js +46 -0
- package/dist/local-dev-personas.js.map +1 -0
- package/dist/mcp.d.ts +567 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2480 -0
- package/dist/mcp.js.map +1 -0
- package/dist/modules.d.ts +145 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +164 -0
- package/dist/modules.js.map +1 -0
- package/dist/notification-client.d.ts +94 -0
- package/dist/notification-client.d.ts.map +1 -0
- package/dist/notification-client.js +127 -0
- package/dist/notification-client.js.map +1 -0
- package/dist/notifications.d.ts +447 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +1625 -0
- package/dist/notifications.js.map +1 -0
- package/dist/operator-data-query.d.ts +117 -0
- package/dist/operator-data-query.d.ts.map +1 -0
- package/dist/operator-data-query.js +431 -0
- package/dist/operator-data-query.js.map +1 -0
- package/dist/operator-mcp.d.ts +63 -0
- package/dist/operator-mcp.d.ts.map +1 -0
- package/dist/operator-mcp.js +2828 -0
- package/dist/operator-mcp.js.map +1 -0
- package/dist/outbound-https.d.ts +24 -0
- package/dist/outbound-https.d.ts.map +1 -0
- package/dist/outbound-https.js +172 -0
- package/dist/outbound-https.js.map +1 -0
- package/dist/pitr.d.ts +60 -0
- package/dist/pitr.d.ts.map +1 -0
- package/dist/pitr.js +94 -0
- package/dist/pitr.js.map +1 -0
- package/dist/platform.d.ts +199 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +196 -0
- package/dist/platform.js.map +1 -0
- package/dist/policies.d.ts +115 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +158 -0
- package/dist/policies.js.map +1 -0
- package/dist/principal.d.ts +143 -0
- package/dist/principal.d.ts.map +1 -0
- package/dist/principal.js +174 -0
- package/dist/principal.js.map +1 -0
- package/dist/realtime.d.ts +280 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +578 -0
- package/dist/realtime.js.map +1 -0
- package/dist/retrieval.d.ts +263 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +602 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/runtime-error-client.d.ts +10 -0
- package/dist/runtime-error-client.d.ts.map +1 -0
- package/dist/runtime-error-client.js +70 -0
- package/dist/runtime-error-client.js.map +1 -0
- package/dist/runtime-errors.d.ts +180 -0
- package/dist/runtime-errors.d.ts.map +1 -0
- package/dist/runtime-errors.js +572 -0
- package/dist/runtime-errors.js.map +1 -0
- package/dist/schema.d.ts +434 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +232 -0
- package/dist/schema.js.map +1 -0
- package/dist/search.d.ts +216 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +785 -0
- package/dist/search.js.map +1 -0
- package/dist/settings.d.ts +191 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +478 -0
- package/dist/settings.js.map +1 -0
- package/dist/sql.d.ts +7 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +30 -0
- package/dist/sql.js.map +1 -0
- package/dist/storage-client.d.ts +69 -0
- package/dist/storage-client.d.ts.map +1 -0
- package/dist/storage-client.js +469 -0
- package/dist/storage-client.js.map +1 -0
- package/dist/storage.d.ts +21 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +82 -0
- package/dist/storage.js.map +1 -0
- package/dist/subject-domains.d.ts +15 -0
- package/dist/subject-domains.d.ts.map +1 -0
- package/dist/subject-domains.js +38 -0
- package/dist/subject-domains.js.map +1 -0
- package/dist/support.d.ts +132 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +483 -0
- package/dist/support.js.map +1 -0
- package/dist/system-tables.d.ts +12 -0
- package/dist/system-tables.d.ts.map +1 -0
- package/dist/system-tables.js +225 -0
- package/dist/system-tables.js.map +1 -0
- package/dist/test.d.ts +57 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +454 -0
- package/dist/test.js.map +1 -0
- package/dist/vite.d.ts +82 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +82 -0
- package/dist/vite.js.map +1 -0
- package/dist/webpush-crypto.d.ts +39 -0
- package/dist/webpush-crypto.d.ts.map +1 -0
- package/dist/webpush-crypto.js +213 -0
- package/dist/webpush-crypto.js.map +1 -0
- package/dist/webpush-private.d.ts +11 -0
- package/dist/webpush-private.d.ts.map +1 -0
- package/dist/webpush-private.js +11 -0
- package/dist/webpush-private.js.map +1 -0
- package/dist/webpush.d.ts +86 -0
- package/dist/webpush.d.ts.map +1 -0
- package/dist/webpush.js +375 -0
- package/dist/webpush.js.map +1 -0
- package/dist/worker.d.ts +185 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +13021 -0
- package/dist/worker.js.map +1 -0
- package/package.json +161 -8
- package/README.md +0 -3
|
@@ -0,0 +1,1247 @@
|
|
|
1
|
+
import { RaybaseBackupError, generateBackupDek, unwrapBackupDek, validateBackupJsonMetadata, validateBackupKeyEnvelope, wrapBackupDek } from "./backup-crypto.js";
|
|
2
|
+
export const backupBinaryContentType = "application/vnd.raybase.backup";
|
|
3
|
+
export const backupArchiveMagic = new Uint8Array([0x52, 0x41, 0x59, 0x42, 0x41, 0x53, 0x45, 0x32]); // RAYBASE2
|
|
4
|
+
export const backupArchiveFormatVersion = 2;
|
|
5
|
+
export const backupLogicalFormatVersion = 2;
|
|
6
|
+
export const backupFrameFormatVersion = 1;
|
|
7
|
+
export const backupLogicalRecordVersion = 1;
|
|
8
|
+
export const defaultBackupFramePlaintextBytes = 1024 * 1024;
|
|
9
|
+
export const backupFrameHeaderBytes = 12;
|
|
10
|
+
export const backupFrameTagBytes = 16;
|
|
11
|
+
export const backupFormatLimits = {
|
|
12
|
+
headerBytes: 64 * 1024,
|
|
13
|
+
logicalRecordBytes: 16 * 1024 * 1024,
|
|
14
|
+
ddlStatementBytes: 100 * 1024,
|
|
15
|
+
identifierBytes: 256,
|
|
16
|
+
columnsPerTable: 512,
|
|
17
|
+
valuesPerRow: 512,
|
|
18
|
+
textValueBytes: 16 * 1024 * 1024,
|
|
19
|
+
blobValueBytes: 16 * 1024 * 1024
|
|
20
|
+
};
|
|
21
|
+
const recordKind = {
|
|
22
|
+
"database-metadata": 0x01,
|
|
23
|
+
"table-schema": 0x02,
|
|
24
|
+
ddl: 0x03,
|
|
25
|
+
"table-start": 0x04,
|
|
26
|
+
row: 0x05,
|
|
27
|
+
"table-end": 0x06,
|
|
28
|
+
"logical-end": 0x7f
|
|
29
|
+
};
|
|
30
|
+
const requiredRecordFlag = 0x01;
|
|
31
|
+
const recordHeaderBytes = 6;
|
|
32
|
+
const archiveHeaderPrefixBytes = backupArchiveMagic.byteLength + 4;
|
|
33
|
+
const textEncoder = new TextEncoder();
|
|
34
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
35
|
+
const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
|
|
36
|
+
const base64UrlPattern = /^[A-Za-z0-9_-]+$/;
|
|
37
|
+
const sha256Pattern = /^[a-f0-9]{64}$/;
|
|
38
|
+
function formatError(message, code = "BACKUP_ARCHIVE_CORRUPT") {
|
|
39
|
+
throw new RaybaseBackupError(code, message);
|
|
40
|
+
}
|
|
41
|
+
function plainObject(value, label) {
|
|
42
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
43
|
+
return formatError(`${label} must be an object`);
|
|
44
|
+
}
|
|
45
|
+
const prototype = Object.getPrototypeOf(value);
|
|
46
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
47
|
+
return formatError(`${label} must be an object`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function assertOnlyKeys(value, allowed, label) {
|
|
51
|
+
const allowedSet = new Set(allowed);
|
|
52
|
+
const extra = Object.keys(value).find((key) => !allowedSet.has(key));
|
|
53
|
+
if (extra)
|
|
54
|
+
formatError(`${label} contains unsupported field ${extra}`);
|
|
55
|
+
}
|
|
56
|
+
function boundedString(value, label, maxBytes = backupFormatLimits.identifierBytes) {
|
|
57
|
+
if (typeof value !== "string" || !value || textEncoder.encode(value).byteLength > maxBytes) {
|
|
58
|
+
return formatError(`${label} is missing or exceeds ${maxBytes} bytes`, "BACKUP_LIMIT_EXCEEDED");
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function boundedIdentifier(value, label) {
|
|
63
|
+
const stringValue = boundedString(value, label);
|
|
64
|
+
if (!identifierPattern.test(stringValue))
|
|
65
|
+
return formatError(`${label} is invalid`);
|
|
66
|
+
return stringValue;
|
|
67
|
+
}
|
|
68
|
+
function encodeFormatBase64Url(value) {
|
|
69
|
+
let binary = "";
|
|
70
|
+
for (const byte of value)
|
|
71
|
+
binary += String.fromCharCode(byte);
|
|
72
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
73
|
+
}
|
|
74
|
+
function decodeCanonicalFormatBase64Url(value, label, byteLength) {
|
|
75
|
+
if (typeof value !== "string" || !base64UrlPattern.test(value)) {
|
|
76
|
+
return formatError(`${label} must be canonical unpadded base64url data`);
|
|
77
|
+
}
|
|
78
|
+
const padded = `${value.replace(/-/g, "+").replace(/_/g, "/")}${"=".repeat((4 - value.length % 4) % 4)}`;
|
|
79
|
+
let decoded;
|
|
80
|
+
try {
|
|
81
|
+
decoded = Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return formatError(`${label} must be canonical unpadded base64url data`);
|
|
85
|
+
}
|
|
86
|
+
if (decoded.byteLength !== byteLength || encodeFormatBase64Url(decoded) !== value) {
|
|
87
|
+
decoded.fill(0);
|
|
88
|
+
return formatError(`${label} must encode exactly ${byteLength} bytes canonically`);
|
|
89
|
+
}
|
|
90
|
+
return decoded;
|
|
91
|
+
}
|
|
92
|
+
function nonNegativeInteger(value, label) {
|
|
93
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
94
|
+
return formatError(`${label} must be a non-negative safe integer`);
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function canonicalize(value) {
|
|
98
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
99
|
+
return JSON.stringify(value);
|
|
100
|
+
if (typeof value === "number") {
|
|
101
|
+
if (!Number.isFinite(value))
|
|
102
|
+
formatError("Canonical backup JSON cannot contain a non-finite number");
|
|
103
|
+
return JSON.stringify(value);
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(value))
|
|
106
|
+
return `[${value.map(canonicalize).join(",")}]`;
|
|
107
|
+
const object = plainObject(value, "Canonical backup JSON");
|
|
108
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(object[key])}`).join(",")}}`;
|
|
109
|
+
}
|
|
110
|
+
export function canonicalBackupJson(value) {
|
|
111
|
+
return textEncoder.encode(canonicalize(value));
|
|
112
|
+
}
|
|
113
|
+
function parseJson(bytes, label) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(textDecoder.decode(bytes));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return formatError(`${label} is not valid UTF-8 JSON`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function writeUint32(value) {
|
|
122
|
+
const bytes = new Uint8Array(4);
|
|
123
|
+
new DataView(bytes.buffer).setUint32(0, value, false);
|
|
124
|
+
return bytes;
|
|
125
|
+
}
|
|
126
|
+
function concatBytes(parts) {
|
|
127
|
+
const length = parts.reduce((total, part) => total + part.byteLength, 0);
|
|
128
|
+
const output = new Uint8Array(length);
|
|
129
|
+
let offset = 0;
|
|
130
|
+
for (const part of parts) {
|
|
131
|
+
output.set(part, offset);
|
|
132
|
+
offset += part.byteLength;
|
|
133
|
+
}
|
|
134
|
+
return output;
|
|
135
|
+
}
|
|
136
|
+
function normalizeHeader(value) {
|
|
137
|
+
const object = plainObject(value, "Backup archive header");
|
|
138
|
+
assertOnlyKeys(object, ["format", "archiveId", "product", "target", "targetId", "createdAt", "logical", "encryption", "keyEnvelope"], "Backup archive header");
|
|
139
|
+
if (object.format !== 2)
|
|
140
|
+
formatError("Backup archive format is unsupported", "BACKUP_ARCHIVE_VERSION_UNSUPPORTED");
|
|
141
|
+
if (object.product !== "core" && object.product !== "pro")
|
|
142
|
+
formatError("Backup archive product is unsupported");
|
|
143
|
+
if (object.target !== "core" && object.target !== "control-plane" && object.target !== "app-data")
|
|
144
|
+
formatError("Backup archive target is unsupported");
|
|
145
|
+
const logical = plainObject(object.logical, "Backup logical descriptor");
|
|
146
|
+
assertOnlyKeys(logical, ["format", "recordVersion", "encoding"], "Backup logical descriptor");
|
|
147
|
+
if (logical.format !== 2 || logical.recordVersion !== 1 || logical.encoding !== "raybase-logical") {
|
|
148
|
+
formatError("Backup logical record version is unsupported", "BACKUP_RECORD_VERSION_UNSUPPORTED");
|
|
149
|
+
}
|
|
150
|
+
const encryption = plainObject(object.encryption, "Backup encryption descriptor");
|
|
151
|
+
assertOnlyKeys(encryption, ["scheme", "frameVersion", "framePlaintextBytes", "noncePrefix"], "Backup encryption descriptor");
|
|
152
|
+
if (encryption.scheme !== "AES-256-GCM" || encryption.frameVersion !== 1) {
|
|
153
|
+
formatError("Backup frame version is unsupported", "BACKUP_FRAME_VERSION_UNSUPPORTED");
|
|
154
|
+
}
|
|
155
|
+
if (!Number.isInteger(encryption.framePlaintextBytes) || encryption.framePlaintextBytes < 1024 || encryption.framePlaintextBytes > 16 * 1024 * 1024) {
|
|
156
|
+
formatError("Backup frame plaintext size is invalid", "BACKUP_LIMIT_EXCEEDED");
|
|
157
|
+
}
|
|
158
|
+
const noncePrefixBytes = decodeCanonicalFormatBase64Url(encryption.noncePrefix, "Backup nonce prefix", 8);
|
|
159
|
+
noncePrefixBytes.fill(0);
|
|
160
|
+
const createdAt = boundedString(object.createdAt, "Backup created time", 64);
|
|
161
|
+
if (!Number.isFinite(Date.parse(createdAt)))
|
|
162
|
+
formatError("Backup created time is invalid");
|
|
163
|
+
return {
|
|
164
|
+
format: 2,
|
|
165
|
+
archiveId: boundedIdentifier(object.archiveId, "Backup archive id"),
|
|
166
|
+
product: object.product,
|
|
167
|
+
target: object.target,
|
|
168
|
+
targetId: boundedIdentifier(object.targetId, "Backup target id"),
|
|
169
|
+
createdAt,
|
|
170
|
+
logical: { format: 2, recordVersion: 1, encoding: "raybase-logical" },
|
|
171
|
+
encryption: {
|
|
172
|
+
scheme: "AES-256-GCM",
|
|
173
|
+
frameVersion: 1,
|
|
174
|
+
framePlaintextBytes: encryption.framePlaintextBytes,
|
|
175
|
+
noncePrefix: encryption.noncePrefix
|
|
176
|
+
},
|
|
177
|
+
keyEnvelope: validateBackupKeyEnvelope(object.keyEnvelope)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Encode the cleartext, authenticated format-2 archive header.
|
|
182
|
+
*/
|
|
183
|
+
export function encodeBackupArchiveHeader(header) {
|
|
184
|
+
const normalized = normalizeHeader(header);
|
|
185
|
+
const body = canonicalBackupJson(normalized);
|
|
186
|
+
if (body.byteLength > backupFormatLimits.headerBytes)
|
|
187
|
+
formatError("Backup archive header exceeds its byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
188
|
+
return concatBytes([backupArchiveMagic, writeUint32(body.byteLength), body]);
|
|
189
|
+
}
|
|
190
|
+
export function decodeBackupArchiveHeader(bytes) {
|
|
191
|
+
if (bytes.byteLength < archiveHeaderPrefixBytes)
|
|
192
|
+
formatError("Backup archive header is truncated", "BACKUP_ARCHIVE_TRUNCATED");
|
|
193
|
+
for (let index = 0; index < backupArchiveMagic.byteLength; index += 1) {
|
|
194
|
+
if (bytes[index] !== backupArchiveMagic[index])
|
|
195
|
+
formatError("Backup archive magic is invalid", "BACKUP_ARCHIVE_VERSION_UNSUPPORTED");
|
|
196
|
+
}
|
|
197
|
+
const bodyLength = new DataView(bytes.buffer, bytes.byteOffset + backupArchiveMagic.byteLength, 4).getUint32(0, false);
|
|
198
|
+
if (bodyLength > backupFormatLimits.headerBytes)
|
|
199
|
+
formatError("Backup archive header exceeds its byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
200
|
+
const bytesRead = archiveHeaderPrefixBytes + bodyLength;
|
|
201
|
+
if (bytes.byteLength < bytesRead)
|
|
202
|
+
formatError("Backup archive header is truncated", "BACKUP_ARCHIVE_TRUNCATED");
|
|
203
|
+
const body = bytes.subarray(archiveHeaderPrefixBytes, bytesRead);
|
|
204
|
+
const header = normalizeHeader(parseJson(body, "Backup archive header"));
|
|
205
|
+
const canonical = canonicalBackupJson(header);
|
|
206
|
+
if (canonical.byteLength !== body.byteLength || canonical.some((value, index) => value !== body[index])) {
|
|
207
|
+
formatError("Backup archive header is not canonically encoded");
|
|
208
|
+
}
|
|
209
|
+
return { header, bytesRead };
|
|
210
|
+
}
|
|
211
|
+
function normalizeDatabaseMetadata(value) {
|
|
212
|
+
const object = plainObject(value, "Database metadata record");
|
|
213
|
+
assertOnlyKeys(object, ["kind", "applicationId", "userVersion", "schemaFingerprint", "metadata"], "Database metadata record");
|
|
214
|
+
const record = {
|
|
215
|
+
kind: "database-metadata",
|
|
216
|
+
schemaFingerprint: boundedIdentifier(object.schemaFingerprint, "Database schema fingerprint")
|
|
217
|
+
};
|
|
218
|
+
if (object.applicationId !== undefined)
|
|
219
|
+
record.applicationId = nonNegativeInteger(object.applicationId, "Database application id");
|
|
220
|
+
if (object.userVersion !== undefined)
|
|
221
|
+
record.userVersion = nonNegativeInteger(object.userVersion, "Database user version");
|
|
222
|
+
if (object.metadata !== undefined)
|
|
223
|
+
record.metadata = validateBackupJsonMetadata(object.metadata, "Database backup metadata");
|
|
224
|
+
return record;
|
|
225
|
+
}
|
|
226
|
+
function normalizeColumn(value, index) {
|
|
227
|
+
const object = plainObject(value, `Table column ${index}`);
|
|
228
|
+
assertOnlyKeys(object, ["name", "declaredType"], `Table column ${index}`);
|
|
229
|
+
const column = { name: boundedString(object.name, `Table column ${index} name`) };
|
|
230
|
+
if (object.declaredType !== undefined)
|
|
231
|
+
column.declaredType = boundedString(object.declaredType, `Table column ${index} declared type`, 128);
|
|
232
|
+
return column;
|
|
233
|
+
}
|
|
234
|
+
function normalizeTableSchema(value) {
|
|
235
|
+
const object = plainObject(value, "Table schema record");
|
|
236
|
+
assertOnlyKeys(object, ["kind", "tableId", "tableName", "columns", "insertSql", "pagination"], "Table schema record");
|
|
237
|
+
if (!Array.isArray(object.columns) || object.columns.length < 1 || object.columns.length > backupFormatLimits.columnsPerTable) {
|
|
238
|
+
formatError("Table schema column count is invalid", "BACKUP_LIMIT_EXCEEDED");
|
|
239
|
+
}
|
|
240
|
+
const columns = object.columns.map(normalizeColumn);
|
|
241
|
+
if (new Set(columns.map((column) => column.name)).size !== columns.length)
|
|
242
|
+
formatError("Table schema column names must be unique");
|
|
243
|
+
const pagination = plainObject(object.pagination, "Table pagination descriptor");
|
|
244
|
+
const paginationKind = pagination.kind;
|
|
245
|
+
let normalizedPagination;
|
|
246
|
+
if (paginationKind === "rowid") {
|
|
247
|
+
assertOnlyKeys(pagination, ["kind", "column"], "Table pagination descriptor");
|
|
248
|
+
normalizedPagination = { kind: "rowid", column: boundedString(pagination.column, "Table rowid column") };
|
|
249
|
+
}
|
|
250
|
+
else if (paginationKind === "primary-key") {
|
|
251
|
+
assertOnlyKeys(pagination, ["kind", "columns"], "Table pagination descriptor");
|
|
252
|
+
if (!Array.isArray(pagination.columns) || pagination.columns.length < 1 || pagination.columns.length > columns.length) {
|
|
253
|
+
formatError("Table primary-key pagination columns are invalid");
|
|
254
|
+
}
|
|
255
|
+
normalizedPagination = {
|
|
256
|
+
kind: "primary-key",
|
|
257
|
+
columns: pagination.columns.map((column, index) => boundedString(column, `Table primary-key column ${index}`))
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
return formatError("Table pagination kind is unsupported");
|
|
262
|
+
}
|
|
263
|
+
const insertSql = boundedString(object.insertSql, "Table insert statement", backupFormatLimits.ddlStatementBytes);
|
|
264
|
+
return {
|
|
265
|
+
kind: "table-schema",
|
|
266
|
+
tableId: boundedIdentifier(object.tableId, "Table id"),
|
|
267
|
+
tableName: boundedString(object.tableName, "Table name"),
|
|
268
|
+
columns,
|
|
269
|
+
insertSql,
|
|
270
|
+
pagination: normalizedPagination
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function normalizeDdl(value) {
|
|
274
|
+
const object = plainObject(value, "DDL record");
|
|
275
|
+
assertOnlyKeys(object, ["kind", "sql"], "DDL record");
|
|
276
|
+
return { kind: "ddl", sql: boundedString(object.sql, "DDL statement", backupFormatLimits.ddlStatementBytes) };
|
|
277
|
+
}
|
|
278
|
+
function normalizeBoundary(value, kind) {
|
|
279
|
+
const object = plainObject(value, `${kind} record`);
|
|
280
|
+
assertOnlyKeys(object, ["kind", "tableId"], `${kind} record`);
|
|
281
|
+
return { kind, tableId: boundedIdentifier(object.tableId, `${kind} table id`) };
|
|
282
|
+
}
|
|
283
|
+
function normalizeLogicalEnd(value) {
|
|
284
|
+
const object = plainObject(value, "Logical end record");
|
|
285
|
+
assertOnlyKeys(object, ["kind", "recordCount", "rowCount", "tableCount", "plaintextSha256"], "Logical end record");
|
|
286
|
+
if (typeof object.plaintextSha256 !== "string" || !sha256Pattern.test(object.plaintextSha256)) {
|
|
287
|
+
formatError("Logical end plaintext digest is invalid");
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
kind: "logical-end",
|
|
291
|
+
recordCount: nonNegativeInteger(object.recordCount, "Logical record count"),
|
|
292
|
+
rowCount: nonNegativeInteger(object.rowCount, "Logical row count"),
|
|
293
|
+
tableCount: nonNegativeInteger(object.tableCount, "Logical table count"),
|
|
294
|
+
plaintextSha256: object.plaintextSha256
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function encodeTypedValue(value) {
|
|
298
|
+
switch (value.type) {
|
|
299
|
+
case "null":
|
|
300
|
+
return new Uint8Array([0]);
|
|
301
|
+
case "integer": {
|
|
302
|
+
if (value.value < -(1n << 63n) || value.value > (1n << 63n) - 1n)
|
|
303
|
+
formatError("Logical integer is outside signed 64-bit range");
|
|
304
|
+
const bytes = new Uint8Array(9);
|
|
305
|
+
bytes[0] = 1;
|
|
306
|
+
new DataView(bytes.buffer).setBigInt64(1, value.value, false);
|
|
307
|
+
return bytes;
|
|
308
|
+
}
|
|
309
|
+
case "real": {
|
|
310
|
+
if (!Number.isFinite(value.value))
|
|
311
|
+
formatError("Logical real must be finite");
|
|
312
|
+
const bytes = new Uint8Array(9);
|
|
313
|
+
bytes[0] = 2;
|
|
314
|
+
new DataView(bytes.buffer).setFloat64(1, value.value, false);
|
|
315
|
+
return bytes;
|
|
316
|
+
}
|
|
317
|
+
case "text": {
|
|
318
|
+
const body = textEncoder.encode(value.value);
|
|
319
|
+
if (body.byteLength > backupFormatLimits.textValueBytes)
|
|
320
|
+
formatError("Logical text exceeds its byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
321
|
+
return concatBytes([new Uint8Array([3]), writeUint32(body.byteLength), body]);
|
|
322
|
+
}
|
|
323
|
+
case "blob": {
|
|
324
|
+
if (!(value.value instanceof Uint8Array))
|
|
325
|
+
formatError("Logical BLOB must be a Uint8Array");
|
|
326
|
+
if (value.value.byteLength > backupFormatLimits.blobValueBytes)
|
|
327
|
+
formatError("Logical BLOB exceeds its byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
328
|
+
return concatBytes([new Uint8Array([4]), writeUint32(value.value.byteLength), value.value]);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function encodeRow(record) {
|
|
333
|
+
const tableId = textEncoder.encode(boundedIdentifier(record.tableId, "Row table id"));
|
|
334
|
+
if (tableId.byteLength > 0xffff)
|
|
335
|
+
formatError("Row table id exceeds its byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
336
|
+
if (!Array.isArray(record.values) || record.values.length > backupFormatLimits.valuesPerRow) {
|
|
337
|
+
formatError("Row value count exceeds its limit", "BACKUP_LIMIT_EXCEEDED");
|
|
338
|
+
}
|
|
339
|
+
const prefix = new Uint8Array(2 + tableId.byteLength + 2);
|
|
340
|
+
const view = new DataView(prefix.buffer);
|
|
341
|
+
view.setUint16(0, tableId.byteLength, false);
|
|
342
|
+
prefix.set(tableId, 2);
|
|
343
|
+
view.setUint16(2 + tableId.byteLength, record.values.length, false);
|
|
344
|
+
return concatBytes([prefix, ...record.values.map(encodeTypedValue)]);
|
|
345
|
+
}
|
|
346
|
+
function readLengthPrefixedValue(payload, state, label, maxBytes) {
|
|
347
|
+
if (state.offset + 4 > payload.byteLength)
|
|
348
|
+
formatError(`${label} length is truncated`, "BACKUP_ARCHIVE_TRUNCATED");
|
|
349
|
+
const length = new DataView(payload.buffer, payload.byteOffset + state.offset, 4).getUint32(0, false);
|
|
350
|
+
state.offset += 4;
|
|
351
|
+
if (length > maxBytes)
|
|
352
|
+
formatError(`${label} exceeds its byte limit`, "BACKUP_LIMIT_EXCEEDED");
|
|
353
|
+
if (state.offset + length > payload.byteLength)
|
|
354
|
+
formatError(`${label} bytes are truncated`, "BACKUP_ARCHIVE_TRUNCATED");
|
|
355
|
+
const value = payload.slice(state.offset, state.offset + length);
|
|
356
|
+
state.offset += length;
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
function decodeTypedValue(payload, state) {
|
|
360
|
+
if (state.offset >= payload.byteLength)
|
|
361
|
+
formatError("Logical value tag is truncated", "BACKUP_ARCHIVE_TRUNCATED");
|
|
362
|
+
const tag = payload[state.offset++];
|
|
363
|
+
if (tag === 0)
|
|
364
|
+
return { type: "null" };
|
|
365
|
+
if (tag === 1) {
|
|
366
|
+
if (state.offset + 8 > payload.byteLength)
|
|
367
|
+
formatError("Logical integer is truncated", "BACKUP_ARCHIVE_TRUNCATED");
|
|
368
|
+
const value = new DataView(payload.buffer, payload.byteOffset + state.offset, 8).getBigInt64(0, false);
|
|
369
|
+
state.offset += 8;
|
|
370
|
+
return { type: "integer", value };
|
|
371
|
+
}
|
|
372
|
+
if (tag === 2) {
|
|
373
|
+
if (state.offset + 8 > payload.byteLength)
|
|
374
|
+
formatError("Logical real is truncated", "BACKUP_ARCHIVE_TRUNCATED");
|
|
375
|
+
const value = new DataView(payload.buffer, payload.byteOffset + state.offset, 8).getFloat64(0, false);
|
|
376
|
+
state.offset += 8;
|
|
377
|
+
if (!Number.isFinite(value))
|
|
378
|
+
formatError("Logical real must be finite");
|
|
379
|
+
return { type: "real", value };
|
|
380
|
+
}
|
|
381
|
+
if (tag === 3) {
|
|
382
|
+
const bytes = readLengthPrefixedValue(payload, state, "Logical text", backupFormatLimits.textValueBytes);
|
|
383
|
+
try {
|
|
384
|
+
return { type: "text", value: textDecoder.decode(bytes) };
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
return formatError("Logical text is not valid UTF-8");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (tag === 4)
|
|
391
|
+
return { type: "blob", value: readLengthPrefixedValue(payload, state, "Logical BLOB", backupFormatLimits.blobValueBytes) };
|
|
392
|
+
return formatError(`Logical value tag ${tag} is unsupported`, "BACKUP_RECORD_VERSION_UNSUPPORTED");
|
|
393
|
+
}
|
|
394
|
+
function decodeRow(payload) {
|
|
395
|
+
if (payload.byteLength < 4)
|
|
396
|
+
formatError("Row record is truncated", "BACKUP_ARCHIVE_TRUNCATED");
|
|
397
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
398
|
+
const tableIdLength = view.getUint16(0, false);
|
|
399
|
+
if (2 + tableIdLength + 2 > payload.byteLength)
|
|
400
|
+
formatError("Row table id is truncated", "BACKUP_ARCHIVE_TRUNCATED");
|
|
401
|
+
let tableId;
|
|
402
|
+
try {
|
|
403
|
+
tableId = textDecoder.decode(payload.subarray(2, 2 + tableIdLength));
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return formatError("Row table id is not valid UTF-8");
|
|
407
|
+
}
|
|
408
|
+
boundedIdentifier(tableId, "Row table id");
|
|
409
|
+
const valueCount = view.getUint16(2 + tableIdLength, false);
|
|
410
|
+
if (valueCount > backupFormatLimits.valuesPerRow)
|
|
411
|
+
formatError("Row value count exceeds its limit", "BACKUP_LIMIT_EXCEEDED");
|
|
412
|
+
const state = { offset: 2 + tableIdLength + 2 };
|
|
413
|
+
const values = [];
|
|
414
|
+
for (let index = 0; index < valueCount; index += 1)
|
|
415
|
+
values.push(decodeTypedValue(payload, state));
|
|
416
|
+
if (state.offset !== payload.byteLength)
|
|
417
|
+
formatError("Row record contains trailing bytes");
|
|
418
|
+
return { kind: "row", tableId, values };
|
|
419
|
+
}
|
|
420
|
+
function encodeRecordPayload(record) {
|
|
421
|
+
switch (record.kind) {
|
|
422
|
+
case "database-metadata":
|
|
423
|
+
return canonicalBackupJson(normalizeDatabaseMetadata(record));
|
|
424
|
+
case "table-schema":
|
|
425
|
+
return canonicalBackupJson(normalizeTableSchema(record));
|
|
426
|
+
case "ddl":
|
|
427
|
+
return canonicalBackupJson(normalizeDdl(record));
|
|
428
|
+
case "table-start":
|
|
429
|
+
case "table-end":
|
|
430
|
+
return canonicalBackupJson(normalizeBoundary(record, record.kind));
|
|
431
|
+
case "row":
|
|
432
|
+
return encodeRow(record);
|
|
433
|
+
case "logical-end":
|
|
434
|
+
return canonicalBackupJson(normalizeLogicalEnd(record));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
export function encodeBackupLogicalRecord(record) {
|
|
438
|
+
const payload = encodeRecordPayload(record);
|
|
439
|
+
if (payload.byteLength > backupFormatLimits.logicalRecordBytes)
|
|
440
|
+
formatError("Logical record exceeds its byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
441
|
+
const header = new Uint8Array(recordHeaderBytes);
|
|
442
|
+
header[0] = recordKind[record.kind];
|
|
443
|
+
header[1] = requiredRecordFlag;
|
|
444
|
+
new DataView(header.buffer).setUint32(2, payload.byteLength, false);
|
|
445
|
+
return concatBytes([header, payload]);
|
|
446
|
+
}
|
|
447
|
+
function decodeJsonRecord(payload, normalize, label) {
|
|
448
|
+
const value = normalize(parseJson(payload, label));
|
|
449
|
+
const canonical = canonicalBackupJson(value);
|
|
450
|
+
if (canonical.byteLength !== payload.byteLength || canonical.some((byte, index) => byte !== payload[index])) {
|
|
451
|
+
formatError(`${label} is not canonically encoded`);
|
|
452
|
+
}
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
export class BackupLogicalDecoder {
|
|
456
|
+
#buffer = new Uint8Array(0);
|
|
457
|
+
#state = {
|
|
458
|
+
metadataSeen: false,
|
|
459
|
+
schemas: new Map(),
|
|
460
|
+
currentTable: null,
|
|
461
|
+
ended: false,
|
|
462
|
+
recordCount: 0,
|
|
463
|
+
rowCount: 0,
|
|
464
|
+
tableCount: 0
|
|
465
|
+
};
|
|
466
|
+
push(chunk) {
|
|
467
|
+
if (!(chunk instanceof Uint8Array))
|
|
468
|
+
formatError("Logical decoder input must be a Uint8Array");
|
|
469
|
+
if (this.#state.ended && chunk.byteLength > 0)
|
|
470
|
+
formatError("Logical stream contains records after its end");
|
|
471
|
+
if (this.#buffer.byteLength + chunk.byteLength > backupFormatLimits.logicalRecordBytes + recordHeaderBytes) {
|
|
472
|
+
formatError("Logical decoder retained bytes exceed the record limit", "BACKUP_LIMIT_EXCEEDED");
|
|
473
|
+
}
|
|
474
|
+
this.#buffer = new Uint8Array(concatBytes([this.#buffer, chunk]));
|
|
475
|
+
const records = [];
|
|
476
|
+
while (this.#buffer.byteLength >= recordHeaderBytes) {
|
|
477
|
+
const view = new DataView(this.#buffer.buffer, this.#buffer.byteOffset, recordHeaderBytes);
|
|
478
|
+
const kind = this.#buffer[0];
|
|
479
|
+
const flags = this.#buffer[1];
|
|
480
|
+
const length = view.getUint32(2, false);
|
|
481
|
+
if ((flags & ~requiredRecordFlag) !== 0)
|
|
482
|
+
formatError("Logical record flags are unsupported", "BACKUP_RECORD_VERSION_UNSUPPORTED");
|
|
483
|
+
if (length > backupFormatLimits.logicalRecordBytes)
|
|
484
|
+
formatError("Logical record exceeds its byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
485
|
+
if (this.#buffer.byteLength < recordHeaderBytes + length)
|
|
486
|
+
break;
|
|
487
|
+
const payload = this.#buffer.slice(recordHeaderBytes, recordHeaderBytes + length);
|
|
488
|
+
this.#buffer = this.#buffer.slice(recordHeaderBytes + length);
|
|
489
|
+
if (this.#state.ended)
|
|
490
|
+
formatError("Logical stream contains records after its end");
|
|
491
|
+
const record = this.#decodeRecord(kind, flags, payload);
|
|
492
|
+
if (record) {
|
|
493
|
+
this.#accept(record);
|
|
494
|
+
records.push(record);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
this.#state.recordCount += 1;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return records;
|
|
501
|
+
}
|
|
502
|
+
finish() {
|
|
503
|
+
if (this.#buffer.byteLength > 0)
|
|
504
|
+
formatError("Logical stream ends inside a record", "BACKUP_ARCHIVE_TRUNCATED");
|
|
505
|
+
if (!this.#state.ended)
|
|
506
|
+
formatError("Logical stream is missing its end record", "BACKUP_ARCHIVE_TRUNCATED");
|
|
507
|
+
}
|
|
508
|
+
#decodeRecord(kind, flags, payload) {
|
|
509
|
+
if (kind === recordKind["database-metadata"])
|
|
510
|
+
return decodeJsonRecord(payload, normalizeDatabaseMetadata, "Database metadata record");
|
|
511
|
+
if (kind === recordKind["table-schema"])
|
|
512
|
+
return decodeJsonRecord(payload, normalizeTableSchema, "Table schema record");
|
|
513
|
+
if (kind === recordKind.ddl)
|
|
514
|
+
return decodeJsonRecord(payload, normalizeDdl, "DDL record");
|
|
515
|
+
if (kind === recordKind["table-start"])
|
|
516
|
+
return decodeJsonRecord(payload, (value) => normalizeBoundary(value, "table-start"), "Table start record");
|
|
517
|
+
if (kind === recordKind.row)
|
|
518
|
+
return decodeRow(payload);
|
|
519
|
+
if (kind === recordKind["table-end"])
|
|
520
|
+
return decodeJsonRecord(payload, (value) => normalizeBoundary(value, "table-end"), "Table end record");
|
|
521
|
+
if (kind === recordKind["logical-end"])
|
|
522
|
+
return decodeJsonRecord(payload, normalizeLogicalEnd, "Logical end record");
|
|
523
|
+
if ((flags & requiredRecordFlag) !== 0)
|
|
524
|
+
formatError(`Required logical record kind ${kind} is unsupported`, "BACKUP_RECORD_VERSION_UNSUPPORTED");
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
#accept(record) {
|
|
528
|
+
if (this.#state.ended)
|
|
529
|
+
formatError("Logical stream contains records after its end");
|
|
530
|
+
if (record.kind === "logical-end") {
|
|
531
|
+
if (this.#state.currentTable)
|
|
532
|
+
formatError("Logical stream ended before the current table closed");
|
|
533
|
+
if (record.recordCount !== this.#state.recordCount ||
|
|
534
|
+
record.rowCount !== this.#state.rowCount ||
|
|
535
|
+
record.tableCount !== this.#state.tableCount) {
|
|
536
|
+
formatError("Logical end totals do not match decoded records");
|
|
537
|
+
}
|
|
538
|
+
this.#state.ended = true;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
this.#state.recordCount += 1;
|
|
542
|
+
if (record.kind === "database-metadata") {
|
|
543
|
+
if (this.#state.metadataSeen)
|
|
544
|
+
formatError("Logical stream contains duplicate database metadata");
|
|
545
|
+
if (this.#state.schemas.size > 0 || this.#state.currentTable)
|
|
546
|
+
formatError("Database metadata must precede table records");
|
|
547
|
+
this.#state.metadataSeen = true;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (record.kind === "table-schema") {
|
|
551
|
+
if (this.#state.currentTable)
|
|
552
|
+
formatError("Table schema cannot appear inside table rows");
|
|
553
|
+
if (this.#state.schemas.has(record.tableId))
|
|
554
|
+
formatError(`Logical stream contains duplicate schema ${record.tableId}`);
|
|
555
|
+
this.#state.schemas.set(record.tableId, record);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (record.kind === "ddl") {
|
|
559
|
+
if (this.#state.currentTable)
|
|
560
|
+
formatError("DDL cannot appear inside table rows");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (record.kind === "table-start") {
|
|
564
|
+
if (this.#state.currentTable)
|
|
565
|
+
formatError("Logical stream contains nested table starts");
|
|
566
|
+
if (!this.#state.schemas.has(record.tableId))
|
|
567
|
+
formatError(`Table ${record.tableId} started before its schema`);
|
|
568
|
+
this.#state.currentTable = record.tableId;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (record.kind === "row") {
|
|
572
|
+
const schema = this.#state.schemas.get(record.tableId);
|
|
573
|
+
if (!schema)
|
|
574
|
+
formatError(`Row for ${record.tableId} appeared before its schema`);
|
|
575
|
+
if (this.#state.currentTable !== record.tableId)
|
|
576
|
+
formatError(`Row for ${record.tableId} appeared outside its table boundary`);
|
|
577
|
+
if (record.values.length !== schema.columns.length)
|
|
578
|
+
formatError(`Row for ${record.tableId} has the wrong value count`);
|
|
579
|
+
this.#state.rowCount += 1;
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (record.kind === "table-end") {
|
|
583
|
+
if (this.#state.currentTable !== record.tableId)
|
|
584
|
+
formatError(`Table end for ${record.tableId} does not match the active table`);
|
|
585
|
+
this.#state.currentTable = null;
|
|
586
|
+
this.#state.tableCount += 1;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const backupDataFrameKind = 1;
|
|
591
|
+
const backupFinalFrameKind = 2;
|
|
592
|
+
const backupFinalFrameMaxBytes = 4096;
|
|
593
|
+
const maximumUint32 = 0xffff_ffff;
|
|
594
|
+
const frameAadDomain = textEncoder.encode("raybase-backup-frame-v1\0");
|
|
595
|
+
function backupOwnedBuffer(value) {
|
|
596
|
+
const copy = new Uint8Array(value.byteLength);
|
|
597
|
+
copy.set(value);
|
|
598
|
+
return copy.buffer;
|
|
599
|
+
}
|
|
600
|
+
function sha256HexBytes(value) {
|
|
601
|
+
return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
602
|
+
}
|
|
603
|
+
function equalBackupBytes(left, right) {
|
|
604
|
+
if (left.byteLength !== right.byteLength)
|
|
605
|
+
return false;
|
|
606
|
+
let difference = 0;
|
|
607
|
+
for (let index = 0; index < left.byteLength; index += 1)
|
|
608
|
+
difference |= left[index] ^ right[index];
|
|
609
|
+
return difference === 0;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Workers-native bounded SHA-256 sink. Node-based unit tests can inject an
|
|
613
|
+
* equivalent incremental factory without changing production randomness or
|
|
614
|
+
* buffering an archive.
|
|
615
|
+
*/
|
|
616
|
+
export function createBackupSha256Digest() {
|
|
617
|
+
const DigestStreamConstructor = crypto.DigestStream;
|
|
618
|
+
if (typeof DigestStreamConstructor === "undefined") {
|
|
619
|
+
throw new RaybaseBackupError("BACKUP_ENCRYPTION_NOT_CONFIGURED", "This runtime does not provide the streaming SHA-256 primitive required for encrypted backups");
|
|
620
|
+
}
|
|
621
|
+
const stream = new DigestStreamConstructor("SHA-256");
|
|
622
|
+
const writer = stream.getWriter();
|
|
623
|
+
let finished = false;
|
|
624
|
+
return {
|
|
625
|
+
async write(bytes) {
|
|
626
|
+
if (finished)
|
|
627
|
+
throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup digest is already finalized");
|
|
628
|
+
await writer.write(backupOwnedBuffer(bytes));
|
|
629
|
+
},
|
|
630
|
+
async finish() {
|
|
631
|
+
if (finished)
|
|
632
|
+
throw new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup digest is already finalized");
|
|
633
|
+
finished = true;
|
|
634
|
+
try {
|
|
635
|
+
await writer.close();
|
|
636
|
+
return new Uint8Array(await stream.digest);
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
try {
|
|
640
|
+
await stream.digest;
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
// Observe the paired digest rejection before preserving the primary
|
|
644
|
+
// close/finalization failure.
|
|
645
|
+
}
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
async abort(reason) {
|
|
650
|
+
if (finished)
|
|
651
|
+
return;
|
|
652
|
+
finished = true;
|
|
653
|
+
try {
|
|
654
|
+
await writer.abort(reason);
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
// Digest teardown is best effort after the primary failure.
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
await stream.digest;
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
// Aborting a DigestStream rejects its digest promise with the same
|
|
664
|
+
// primary reason; observe it so runtimes do not report it unhandled.
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function deferred() {
|
|
670
|
+
let resolve;
|
|
671
|
+
let reject;
|
|
672
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
673
|
+
resolve = resolvePromise;
|
|
674
|
+
reject = rejectPromise;
|
|
675
|
+
});
|
|
676
|
+
return { promise, resolve, reject };
|
|
677
|
+
}
|
|
678
|
+
function assertSafeCounter(value, label) {
|
|
679
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
680
|
+
formatError(`${label} exceeds the safe integer limit`, "BACKUP_LIMIT_EXCEEDED");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function normalizeLogicalTotals(value) {
|
|
684
|
+
return {
|
|
685
|
+
recordCount: nonNegativeInteger(value.recordCount, "Backup logical record count"),
|
|
686
|
+
rowCount: nonNegativeInteger(value.rowCount, "Backup logical row count"),
|
|
687
|
+
tableCount: nonNegativeInteger(value.tableCount, "Backup logical table count")
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function frameHeader(kind, sequence, plaintextBytes) {
|
|
691
|
+
const bytes = new Uint8Array(backupFrameHeaderBytes);
|
|
692
|
+
const view = new DataView(bytes.buffer);
|
|
693
|
+
bytes[0] = backupFrameFormatVersion;
|
|
694
|
+
bytes[1] = kind;
|
|
695
|
+
view.setUint16(2, 0, false);
|
|
696
|
+
view.setUint32(4, sequence, false);
|
|
697
|
+
view.setUint32(8, plaintextBytes, false);
|
|
698
|
+
return bytes;
|
|
699
|
+
}
|
|
700
|
+
function frameNonce(prefix, sequence) {
|
|
701
|
+
const nonce = new Uint8Array(12);
|
|
702
|
+
nonce.set(prefix, 0);
|
|
703
|
+
new DataView(nonce.buffer).setUint32(8, sequence, false);
|
|
704
|
+
return nonce;
|
|
705
|
+
}
|
|
706
|
+
function frameAdditionalData(headerSha256, header) {
|
|
707
|
+
return concatBytes([frameAadDomain, headerSha256, header]);
|
|
708
|
+
}
|
|
709
|
+
function decodeCanonicalFinalFrame(value) {
|
|
710
|
+
const parsed = plainObject(parseJson(value, "Backup final frame"), "Backup final frame");
|
|
711
|
+
assertOnlyKeys(parsed, ["version", "frameCount", "plaintextBytes", "recordCount", "rowCount", "tableCount", "plaintextSha256"], "Backup final frame");
|
|
712
|
+
if (parsed.version !== 1)
|
|
713
|
+
formatError("Backup final frame version is unsupported", "BACKUP_FRAME_VERSION_UNSUPPORTED");
|
|
714
|
+
if (typeof parsed.plaintextSha256 !== "string" || !sha256Pattern.test(parsed.plaintextSha256)) {
|
|
715
|
+
formatError("Backup final frame plaintext digest is invalid");
|
|
716
|
+
}
|
|
717
|
+
const normalized = {
|
|
718
|
+
version: 1,
|
|
719
|
+
frameCount: nonNegativeInteger(parsed.frameCount, "Backup final frame count"),
|
|
720
|
+
plaintextBytes: nonNegativeInteger(parsed.plaintextBytes, "Backup final plaintext bytes"),
|
|
721
|
+
recordCount: nonNegativeInteger(parsed.recordCount, "Backup final logical record count"),
|
|
722
|
+
rowCount: nonNegativeInteger(parsed.rowCount, "Backup final logical row count"),
|
|
723
|
+
tableCount: nonNegativeInteger(parsed.tableCount, "Backup final logical table count"),
|
|
724
|
+
plaintextSha256: parsed.plaintextSha256
|
|
725
|
+
};
|
|
726
|
+
const canonical = canonicalBackupJson(normalized);
|
|
727
|
+
if (!equalBackupBytes(canonical, value))
|
|
728
|
+
formatError("Backup final frame is not canonically encoded");
|
|
729
|
+
return normalized;
|
|
730
|
+
}
|
|
731
|
+
async function importBackupFrameKey(raw, usage) {
|
|
732
|
+
return crypto.subtle.importKey("raw", backupOwnedBuffer(raw), { name: "AES-GCM" }, false, [usage]);
|
|
733
|
+
}
|
|
734
|
+
function operationContext(identity, purpose, signal, environment) {
|
|
735
|
+
return {
|
|
736
|
+
...identity,
|
|
737
|
+
purpose,
|
|
738
|
+
signal,
|
|
739
|
+
...(environment ? { environment } : {})
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
export async function createBackupArchiveEncryptor(options) {
|
|
743
|
+
const framePlaintextBytes = options.framePlaintextBytes ?? defaultBackupFramePlaintextBytes;
|
|
744
|
+
if (!Number.isInteger(framePlaintextBytes) || framePlaintextBytes < 1024 || framePlaintextBytes > 16 * 1024 * 1024) {
|
|
745
|
+
formatError("Backup frame plaintext size is invalid", "BACKUP_LIMIT_EXCEEDED");
|
|
746
|
+
}
|
|
747
|
+
const maximumFrameSequence = options.maximumFrameSequence ?? maximumUint32;
|
|
748
|
+
if (!Number.isInteger(maximumFrameSequence) || maximumFrameSequence < 0 || maximumFrameSequence > maximumUint32) {
|
|
749
|
+
formatError("Backup frame sequence limit is invalid", "BACKUP_LIMIT_EXCEEDED");
|
|
750
|
+
}
|
|
751
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
752
|
+
if (signal.aborted)
|
|
753
|
+
throw new RaybaseBackupError("BACKUP_PROVIDER_UNAVAILABLE", "Backup encryption was aborted");
|
|
754
|
+
const context = operationContext(options.identity, "wrap", signal, options.environment);
|
|
755
|
+
const dek = generateBackupDek(options.randomSource);
|
|
756
|
+
let envelope;
|
|
757
|
+
let frameKey;
|
|
758
|
+
try {
|
|
759
|
+
envelope = await wrapBackupDek({
|
|
760
|
+
dek,
|
|
761
|
+
context,
|
|
762
|
+
providers: options.providers,
|
|
763
|
+
...(options.participants ? { participants: options.participants } : {}),
|
|
764
|
+
...(options.randomSource ? { randomSource: options.randomSource } : {})
|
|
765
|
+
});
|
|
766
|
+
frameKey = await importBackupFrameKey(dek, "encrypt");
|
|
767
|
+
}
|
|
768
|
+
finally {
|
|
769
|
+
dek.fill(0);
|
|
770
|
+
}
|
|
771
|
+
const noncePrefix = options.randomSource
|
|
772
|
+
? options.randomSource(8).slice()
|
|
773
|
+
: crypto.getRandomValues(new Uint8Array(8));
|
|
774
|
+
if (noncePrefix.byteLength !== 8)
|
|
775
|
+
formatError("Backup randomness source must return exactly 8 bytes", "BACKUP_LIMIT_EXCEEDED");
|
|
776
|
+
const header = {
|
|
777
|
+
format: 2,
|
|
778
|
+
...options.identity,
|
|
779
|
+
logical: {
|
|
780
|
+
format: 2,
|
|
781
|
+
recordVersion: 1,
|
|
782
|
+
encoding: "raybase-logical"
|
|
783
|
+
},
|
|
784
|
+
encryption: {
|
|
785
|
+
scheme: "AES-256-GCM",
|
|
786
|
+
frameVersion: 1,
|
|
787
|
+
framePlaintextBytes,
|
|
788
|
+
noncePrefix: encodeFormatBase64Url(noncePrefix)
|
|
789
|
+
},
|
|
790
|
+
keyEnvelope: envelope
|
|
791
|
+
};
|
|
792
|
+
noncePrefix.fill(0);
|
|
793
|
+
const headerBytes = encodeBackupArchiveHeader(header);
|
|
794
|
+
const headerSha256 = new Uint8Array(await crypto.subtle.digest("SHA-256", backupOwnedBuffer(headerBytes)));
|
|
795
|
+
const decodedNoncePrefix = decodeCanonicalFormatBase64Url(header.encryption.noncePrefix, "Backup nonce prefix", 8);
|
|
796
|
+
const digestFactory = options.digestFactory ?? createBackupSha256Digest;
|
|
797
|
+
const plaintextDigest = digestFactory();
|
|
798
|
+
const ciphertextDigest = digestFactory();
|
|
799
|
+
const completionState = deferred();
|
|
800
|
+
let sequence = 0;
|
|
801
|
+
let plaintextBytes = 0;
|
|
802
|
+
let ciphertextBytes = 0;
|
|
803
|
+
let pending = new Uint8Array(0);
|
|
804
|
+
let failed = false;
|
|
805
|
+
const abortDigests = async (reason) => {
|
|
806
|
+
await Promise.allSettled([
|
|
807
|
+
plaintextDigest.abort(reason),
|
|
808
|
+
ciphertextDigest.abort(reason)
|
|
809
|
+
]);
|
|
810
|
+
};
|
|
811
|
+
const emitCiphertext = async (controller, bytes) => {
|
|
812
|
+
assertSafeCounter(ciphertextBytes + bytes.byteLength, "Backup ciphertext byte count");
|
|
813
|
+
await ciphertextDigest.write(bytes);
|
|
814
|
+
ciphertextBytes += bytes.byteLength;
|
|
815
|
+
controller.enqueue(bytes);
|
|
816
|
+
};
|
|
817
|
+
const encryptFrame = async (kind, framePlaintext, controller) => {
|
|
818
|
+
if (sequence > maximumFrameSequence) {
|
|
819
|
+
formatError("Backup frame sequence space is exhausted before finalization", "BACKUP_LIMIT_EXCEEDED");
|
|
820
|
+
}
|
|
821
|
+
if (kind === backupDataFrameKind && sequence >= maximumFrameSequence) {
|
|
822
|
+
formatError("Backup frame sequence space is exhausted before nonce reuse", "BACKUP_LIMIT_EXCEEDED");
|
|
823
|
+
}
|
|
824
|
+
const wireHeader = frameHeader(kind, sequence, framePlaintext.byteLength);
|
|
825
|
+
const nonce = frameNonce(decodedNoncePrefix, sequence);
|
|
826
|
+
let ciphertext;
|
|
827
|
+
try {
|
|
828
|
+
ciphertext = new Uint8Array(await crypto.subtle.encrypt({
|
|
829
|
+
name: "AES-GCM",
|
|
830
|
+
iv: backupOwnedBuffer(nonce),
|
|
831
|
+
additionalData: backupOwnedBuffer(frameAdditionalData(headerSha256, wireHeader)),
|
|
832
|
+
tagLength: 128
|
|
833
|
+
}, frameKey, backupOwnedBuffer(framePlaintext)));
|
|
834
|
+
}
|
|
835
|
+
finally {
|
|
836
|
+
nonce.fill(0);
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
await emitCiphertext(controller, concatBytes([wireHeader, ciphertext]));
|
|
840
|
+
}
|
|
841
|
+
finally {
|
|
842
|
+
ciphertext.fill(0);
|
|
843
|
+
}
|
|
844
|
+
sequence += 1;
|
|
845
|
+
};
|
|
846
|
+
const transform = new TransformStream({
|
|
847
|
+
async start(controller) {
|
|
848
|
+
try {
|
|
849
|
+
await emitCiphertext(controller, headerBytes);
|
|
850
|
+
}
|
|
851
|
+
catch (error) {
|
|
852
|
+
failed = true;
|
|
853
|
+
decodedNoncePrefix.fill(0);
|
|
854
|
+
headerSha256.fill(0);
|
|
855
|
+
await abortDigests(error);
|
|
856
|
+
completionState.reject(error);
|
|
857
|
+
throw error;
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
async transform(chunk, controller) {
|
|
861
|
+
try {
|
|
862
|
+
if (signal.aborted)
|
|
863
|
+
throw new RaybaseBackupError("BACKUP_PROVIDER_UNAVAILABLE", "Backup encryption was aborted");
|
|
864
|
+
if (!(chunk instanceof Uint8Array))
|
|
865
|
+
formatError("Backup encryptor input must be a Uint8Array");
|
|
866
|
+
if (chunk.byteLength === 0)
|
|
867
|
+
return;
|
|
868
|
+
assertSafeCounter(plaintextBytes + chunk.byteLength, "Backup plaintext byte count");
|
|
869
|
+
for (let digestOffset = 0; digestOffset < chunk.byteLength; digestOffset += framePlaintextBytes) {
|
|
870
|
+
await plaintextDigest.write(chunk.subarray(digestOffset, Math.min(digestOffset + framePlaintextBytes, chunk.byteLength)));
|
|
871
|
+
}
|
|
872
|
+
plaintextBytes += chunk.byteLength;
|
|
873
|
+
let offset = 0;
|
|
874
|
+
if (pending.byteLength > 0) {
|
|
875
|
+
const take = Math.min(framePlaintextBytes - pending.byteLength, chunk.byteLength);
|
|
876
|
+
pending = concatBytes([pending, chunk.subarray(0, take)]);
|
|
877
|
+
offset = take;
|
|
878
|
+
if (pending.byteLength === framePlaintextBytes) {
|
|
879
|
+
await encryptFrame(backupDataFrameKind, pending, controller);
|
|
880
|
+
pending.fill(0);
|
|
881
|
+
pending = new Uint8Array(0);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
while (chunk.byteLength - offset >= framePlaintextBytes) {
|
|
885
|
+
await encryptFrame(backupDataFrameKind, chunk.subarray(offset, offset + framePlaintextBytes), controller);
|
|
886
|
+
offset += framePlaintextBytes;
|
|
887
|
+
}
|
|
888
|
+
if (offset < chunk.byteLength)
|
|
889
|
+
pending = chunk.slice(offset);
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
failed = true;
|
|
893
|
+
pending.fill(0);
|
|
894
|
+
decodedNoncePrefix.fill(0);
|
|
895
|
+
headerSha256.fill(0);
|
|
896
|
+
await abortDigests(error);
|
|
897
|
+
completionState.reject(error);
|
|
898
|
+
throw error;
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
async flush(controller) {
|
|
902
|
+
if (failed)
|
|
903
|
+
return;
|
|
904
|
+
try {
|
|
905
|
+
if (signal.aborted)
|
|
906
|
+
throw new RaybaseBackupError("BACKUP_PROVIDER_UNAVAILABLE", "Backup encryption was aborted");
|
|
907
|
+
if (pending.byteLength > 0) {
|
|
908
|
+
await encryptFrame(backupDataFrameKind, pending, controller);
|
|
909
|
+
pending.fill(0);
|
|
910
|
+
pending = new Uint8Array(0);
|
|
911
|
+
}
|
|
912
|
+
const plaintextSha256Bytes = await plaintextDigest.finish();
|
|
913
|
+
const plaintextSha256 = sha256HexBytes(plaintextSha256Bytes);
|
|
914
|
+
plaintextSha256Bytes.fill(0);
|
|
915
|
+
const logicalTotals = normalizeLogicalTotals(typeof options.logicalTotals === "function"
|
|
916
|
+
? await options.logicalTotals()
|
|
917
|
+
: options.logicalTotals);
|
|
918
|
+
const finalFrame = {
|
|
919
|
+
version: 1,
|
|
920
|
+
frameCount: sequence,
|
|
921
|
+
plaintextBytes,
|
|
922
|
+
...logicalTotals,
|
|
923
|
+
plaintextSha256
|
|
924
|
+
};
|
|
925
|
+
const finalPlaintext = canonicalBackupJson(finalFrame);
|
|
926
|
+
if (finalPlaintext.byteLength > backupFinalFrameMaxBytes) {
|
|
927
|
+
formatError("Backup final frame exceeds its byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
928
|
+
}
|
|
929
|
+
await encryptFrame(backupFinalFrameKind, finalPlaintext, controller);
|
|
930
|
+
finalPlaintext.fill(0);
|
|
931
|
+
const ciphertextSha256Bytes = await ciphertextDigest.finish();
|
|
932
|
+
const completion = {
|
|
933
|
+
header,
|
|
934
|
+
frameCount: finalFrame.frameCount,
|
|
935
|
+
plaintextBytes,
|
|
936
|
+
plaintextSha256,
|
|
937
|
+
ciphertextBytes,
|
|
938
|
+
ciphertextSha256: sha256HexBytes(ciphertextSha256Bytes),
|
|
939
|
+
logicalTotals
|
|
940
|
+
};
|
|
941
|
+
ciphertextSha256Bytes.fill(0);
|
|
942
|
+
decodedNoncePrefix.fill(0);
|
|
943
|
+
headerSha256.fill(0);
|
|
944
|
+
completionState.resolve(completion);
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
failed = true;
|
|
948
|
+
pending.fill(0);
|
|
949
|
+
decodedNoncePrefix.fill(0);
|
|
950
|
+
headerSha256.fill(0);
|
|
951
|
+
await abortDigests(error);
|
|
952
|
+
completionState.reject(error);
|
|
953
|
+
throw error;
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
async cancel(reason) {
|
|
957
|
+
if (failed)
|
|
958
|
+
return;
|
|
959
|
+
failed = true;
|
|
960
|
+
pending.fill(0);
|
|
961
|
+
decodedNoncePrefix.fill(0);
|
|
962
|
+
headerSha256.fill(0);
|
|
963
|
+
await abortDigests(reason);
|
|
964
|
+
completionState.reject(reason ?? new RaybaseBackupError("BACKUP_ARCHIVE_TRUNCATED", "Backup encryption stream was cancelled"));
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
return {
|
|
968
|
+
header: decodeBackupArchiveHeader(headerBytes).header,
|
|
969
|
+
headerBytes: headerBytes.slice(),
|
|
970
|
+
transform,
|
|
971
|
+
completion: completionState.promise
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function validateExpectedCiphertext(options, bytes, sha256) {
|
|
975
|
+
if (options.expectedCiphertextBytes !== undefined && options.expectedCiphertextBytes !== bytes) {
|
|
976
|
+
formatError("Backup ciphertext byte count does not match its manifest");
|
|
977
|
+
}
|
|
978
|
+
if (options.expectedCiphertextSha256 !== undefined && options.expectedCiphertextSha256 !== sha256) {
|
|
979
|
+
formatError("Backup ciphertext SHA-256 does not match its manifest");
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
export function createBackupArchiveDecryptor(options) {
|
|
983
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
984
|
+
const digestFactory = options.digestFactory ?? createBackupSha256Digest;
|
|
985
|
+
const plaintextDigest = digestFactory();
|
|
986
|
+
const ciphertextDigest = digestFactory();
|
|
987
|
+
const headerState = deferred();
|
|
988
|
+
const completionState = deferred();
|
|
989
|
+
let buffer = new Uint8Array(archiveHeaderPrefixBytes);
|
|
990
|
+
let bufferLength = 0;
|
|
991
|
+
let parsedHeader = null;
|
|
992
|
+
let headerSha256 = new Uint8Array(0);
|
|
993
|
+
let noncePrefix = new Uint8Array(0);
|
|
994
|
+
let frameKey = null;
|
|
995
|
+
let sequence = 0;
|
|
996
|
+
let plaintextBytes = 0;
|
|
997
|
+
let ciphertextBytes = 0;
|
|
998
|
+
let ended = false;
|
|
999
|
+
let failed = false;
|
|
1000
|
+
let finalCompletion = null;
|
|
1001
|
+
const abortDigests = async (reason) => {
|
|
1002
|
+
await Promise.allSettled([
|
|
1003
|
+
plaintextDigest.abort(reason),
|
|
1004
|
+
ciphertextDigest.abort(reason)
|
|
1005
|
+
]);
|
|
1006
|
+
};
|
|
1007
|
+
const failTransform = async (error) => {
|
|
1008
|
+
failed = true;
|
|
1009
|
+
buffer.fill(0);
|
|
1010
|
+
headerSha256.fill(0);
|
|
1011
|
+
noncePrefix.fill(0);
|
|
1012
|
+
await abortDigests(error);
|
|
1013
|
+
headerState.reject(error);
|
|
1014
|
+
completionState.reject(error);
|
|
1015
|
+
throw error;
|
|
1016
|
+
};
|
|
1017
|
+
const expandUnitBuffer = () => {
|
|
1018
|
+
if (bufferLength !== backupFrameHeaderBytes) {
|
|
1019
|
+
formatError("Backup frame decoder prefix state is invalid");
|
|
1020
|
+
}
|
|
1021
|
+
let requiredBytes;
|
|
1022
|
+
if (!parsedHeader) {
|
|
1023
|
+
const bodyLength = new DataView(buffer.buffer, buffer.byteOffset + backupArchiveMagic.byteLength, 4).getUint32(0, false);
|
|
1024
|
+
if (bodyLength > backupFormatLimits.headerBytes) {
|
|
1025
|
+
formatError("Backup archive header exceeds its byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
1026
|
+
}
|
|
1027
|
+
requiredBytes = archiveHeaderPrefixBytes + bodyLength;
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, backupFrameHeaderBytes);
|
|
1031
|
+
const kind = buffer[1];
|
|
1032
|
+
const plaintextLength = view.getUint32(8, false);
|
|
1033
|
+
const maximum = kind === backupFinalFrameKind
|
|
1034
|
+
? backupFinalFrameMaxBytes
|
|
1035
|
+
: parsedHeader.encryption.framePlaintextBytes;
|
|
1036
|
+
if (plaintextLength > maximum)
|
|
1037
|
+
formatError("Backup frame exceeds its plaintext byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
1038
|
+
requiredBytes = backupFrameHeaderBytes + plaintextLength + backupFrameTagBytes;
|
|
1039
|
+
}
|
|
1040
|
+
if (requiredBytes === buffer.byteLength)
|
|
1041
|
+
return;
|
|
1042
|
+
const expanded = new Uint8Array(requiredBytes);
|
|
1043
|
+
expanded.set(buffer.subarray(0, bufferLength));
|
|
1044
|
+
buffer.fill(0);
|
|
1045
|
+
buffer = expanded;
|
|
1046
|
+
};
|
|
1047
|
+
const acceptHeader = async () => {
|
|
1048
|
+
const encoded = buffer.subarray(0, bufferLength);
|
|
1049
|
+
const decoded = decodeBackupArchiveHeader(encoded);
|
|
1050
|
+
if (decoded.bytesRead !== encoded.byteLength)
|
|
1051
|
+
formatError("Backup archive header contains trailing bytes");
|
|
1052
|
+
if (options.validateHeader)
|
|
1053
|
+
await options.validateHeader(decoded.header);
|
|
1054
|
+
const context = operationContext(decoded.header, "unwrap", signal, options.environment);
|
|
1055
|
+
const dek = await unwrapBackupDek({
|
|
1056
|
+
envelope: decoded.header.keyEnvelope,
|
|
1057
|
+
context,
|
|
1058
|
+
providers: options.providers,
|
|
1059
|
+
...(options.providerInput ? { providerInput: options.providerInput } : {})
|
|
1060
|
+
});
|
|
1061
|
+
try {
|
|
1062
|
+
frameKey = await importBackupFrameKey(dek, "decrypt");
|
|
1063
|
+
}
|
|
1064
|
+
finally {
|
|
1065
|
+
dek.fill(0);
|
|
1066
|
+
}
|
|
1067
|
+
parsedHeader = decoded.header;
|
|
1068
|
+
headerSha256 = new Uint8Array(await crypto.subtle.digest("SHA-256", backupOwnedBuffer(encoded)));
|
|
1069
|
+
noncePrefix = decodeCanonicalFormatBase64Url(decoded.header.encryption.noncePrefix, "Backup nonce prefix", 8);
|
|
1070
|
+
buffer.fill(0);
|
|
1071
|
+
buffer = new Uint8Array(backupFrameHeaderBytes);
|
|
1072
|
+
bufferLength = 0;
|
|
1073
|
+
headerState.resolve(decoded.header);
|
|
1074
|
+
};
|
|
1075
|
+
const acceptFrame = async (controller) => {
|
|
1076
|
+
if (!parsedHeader || !frameKey)
|
|
1077
|
+
formatError("Backup frame appeared before its archive header");
|
|
1078
|
+
const wireHeader = buffer.subarray(0, backupFrameHeaderBytes);
|
|
1079
|
+
const view = new DataView(wireHeader.buffer, wireHeader.byteOffset, wireHeader.byteLength);
|
|
1080
|
+
const version = wireHeader[0];
|
|
1081
|
+
const kind = wireHeader[1];
|
|
1082
|
+
const flags = view.getUint16(2, false);
|
|
1083
|
+
const encodedSequence = view.getUint32(4, false);
|
|
1084
|
+
const plaintextLength = view.getUint32(8, false);
|
|
1085
|
+
if (version !== backupFrameFormatVersion) {
|
|
1086
|
+
formatError("Backup frame version is unsupported", "BACKUP_FRAME_VERSION_UNSUPPORTED");
|
|
1087
|
+
}
|
|
1088
|
+
if (kind !== backupDataFrameKind && kind !== backupFinalFrameKind) {
|
|
1089
|
+
formatError("Backup frame kind is unsupported", "BACKUP_FRAME_VERSION_UNSUPPORTED");
|
|
1090
|
+
}
|
|
1091
|
+
if (flags !== 0)
|
|
1092
|
+
formatError("Backup frame flags are unsupported", "BACKUP_FRAME_VERSION_UNSUPPORTED");
|
|
1093
|
+
if (encodedSequence !== sequence)
|
|
1094
|
+
formatError("Backup frame sequence is missing, duplicated, or reordered");
|
|
1095
|
+
if (kind === backupDataFrameKind && sequence >= maximumUint32) {
|
|
1096
|
+
formatError("Backup frame sequence space is exhausted before a final frame", "BACKUP_LIMIT_EXCEEDED");
|
|
1097
|
+
}
|
|
1098
|
+
if ((kind === backupDataFrameKind && plaintextLength > parsedHeader.encryption.framePlaintextBytes) ||
|
|
1099
|
+
(kind === backupFinalFrameKind && plaintextLength > backupFinalFrameMaxBytes)) {
|
|
1100
|
+
formatError("Backup frame exceeds its plaintext byte limit", "BACKUP_LIMIT_EXCEEDED");
|
|
1101
|
+
}
|
|
1102
|
+
const ciphertext = buffer.subarray(backupFrameHeaderBytes, bufferLength);
|
|
1103
|
+
const nonce = frameNonce(noncePrefix, sequence);
|
|
1104
|
+
let plaintext;
|
|
1105
|
+
try {
|
|
1106
|
+
try {
|
|
1107
|
+
plaintext = new Uint8Array(await crypto.subtle.decrypt({
|
|
1108
|
+
name: "AES-GCM",
|
|
1109
|
+
iv: backupOwnedBuffer(nonce),
|
|
1110
|
+
additionalData: backupOwnedBuffer(frameAdditionalData(headerSha256, wireHeader)),
|
|
1111
|
+
tagLength: 128
|
|
1112
|
+
}, frameKey, backupOwnedBuffer(ciphertext)));
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
return formatError("Backup frame failed authenticated decryption");
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
finally {
|
|
1119
|
+
nonce.fill(0);
|
|
1120
|
+
}
|
|
1121
|
+
if (plaintext.byteLength !== plaintextLength) {
|
|
1122
|
+
plaintext.fill(0);
|
|
1123
|
+
formatError("Backup frame plaintext length is invalid");
|
|
1124
|
+
}
|
|
1125
|
+
buffer.fill(0);
|
|
1126
|
+
buffer = new Uint8Array(backupFrameHeaderBytes);
|
|
1127
|
+
bufferLength = 0;
|
|
1128
|
+
if (kind === backupDataFrameKind) {
|
|
1129
|
+
assertSafeCounter(plaintextBytes + plaintext.byteLength, "Backup plaintext byte count");
|
|
1130
|
+
await plaintextDigest.write(plaintext);
|
|
1131
|
+
plaintextBytes += plaintext.byteLength;
|
|
1132
|
+
controller.enqueue(plaintext);
|
|
1133
|
+
sequence += 1;
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const finalFrame = decodeCanonicalFinalFrame(plaintext);
|
|
1137
|
+
plaintext.fill(0);
|
|
1138
|
+
if (finalFrame.frameCount !== sequence ||
|
|
1139
|
+
finalFrame.plaintextBytes !== plaintextBytes) {
|
|
1140
|
+
formatError("Backup final frame totals do not match the decrypted stream");
|
|
1141
|
+
}
|
|
1142
|
+
const plaintextSha256Bytes = await plaintextDigest.finish();
|
|
1143
|
+
const expectedPlaintextSha256 = sha256HexBytes(plaintextSha256Bytes);
|
|
1144
|
+
plaintextSha256Bytes.fill(0);
|
|
1145
|
+
if (finalFrame.plaintextSha256 !== expectedPlaintextSha256) {
|
|
1146
|
+
formatError("Backup plaintext SHA-256 does not match its authenticated final frame");
|
|
1147
|
+
}
|
|
1148
|
+
ended = true;
|
|
1149
|
+
sequence += 1;
|
|
1150
|
+
const ciphertextSha256Bytes = await ciphertextDigest.finish();
|
|
1151
|
+
const ciphertextSha256 = sha256HexBytes(ciphertextSha256Bytes);
|
|
1152
|
+
ciphertextSha256Bytes.fill(0);
|
|
1153
|
+
validateExpectedCiphertext(options, ciphertextBytes, ciphertextSha256);
|
|
1154
|
+
headerSha256.fill(0);
|
|
1155
|
+
noncePrefix.fill(0);
|
|
1156
|
+
finalCompletion = {
|
|
1157
|
+
header: parsedHeader,
|
|
1158
|
+
frameCount: finalFrame.frameCount,
|
|
1159
|
+
plaintextBytes,
|
|
1160
|
+
plaintextSha256: expectedPlaintextSha256,
|
|
1161
|
+
ciphertextBytes,
|
|
1162
|
+
ciphertextSha256,
|
|
1163
|
+
logicalTotals: {
|
|
1164
|
+
recordCount: finalFrame.recordCount,
|
|
1165
|
+
rowCount: finalFrame.rowCount,
|
|
1166
|
+
tableCount: finalFrame.tableCount
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
};
|
|
1170
|
+
const transform = new TransformStream({
|
|
1171
|
+
async transform(chunk, controller) {
|
|
1172
|
+
try {
|
|
1173
|
+
if (signal.aborted)
|
|
1174
|
+
throw new RaybaseBackupError("BACKUP_PROVIDER_UNAVAILABLE", "Backup decryption was aborted");
|
|
1175
|
+
if (!(chunk instanceof Uint8Array))
|
|
1176
|
+
formatError("Backup decryptor input must be a Uint8Array");
|
|
1177
|
+
if (chunk.byteLength === 0)
|
|
1178
|
+
return;
|
|
1179
|
+
if (ended)
|
|
1180
|
+
formatError("Backup archive contains frames or bytes after its final frame");
|
|
1181
|
+
assertSafeCounter(ciphertextBytes + chunk.byteLength, "Backup ciphertext byte count");
|
|
1182
|
+
for (let digestOffset = 0; digestOffset < chunk.byteLength; digestOffset += defaultBackupFramePlaintextBytes) {
|
|
1183
|
+
await ciphertextDigest.write(chunk.subarray(digestOffset, Math.min(digestOffset + defaultBackupFramePlaintextBytes, chunk.byteLength)));
|
|
1184
|
+
}
|
|
1185
|
+
ciphertextBytes += chunk.byteLength;
|
|
1186
|
+
let offset = 0;
|
|
1187
|
+
while (offset < chunk.byteLength) {
|
|
1188
|
+
if (ended)
|
|
1189
|
+
formatError("Backup archive contains frames or bytes after its final frame");
|
|
1190
|
+
const take = Math.min(buffer.byteLength - bufferLength, chunk.byteLength - offset);
|
|
1191
|
+
if (take <= 0)
|
|
1192
|
+
formatError("Backup frame decoder made no progress");
|
|
1193
|
+
buffer.set(chunk.subarray(offset, offset + take), bufferLength);
|
|
1194
|
+
bufferLength += take;
|
|
1195
|
+
offset += take;
|
|
1196
|
+
if (bufferLength === buffer.byteLength) {
|
|
1197
|
+
if (buffer.byteLength === backupFrameHeaderBytes) {
|
|
1198
|
+
expandUnitBuffer();
|
|
1199
|
+
}
|
|
1200
|
+
if (bufferLength === buffer.byteLength) {
|
|
1201
|
+
if (!parsedHeader)
|
|
1202
|
+
await acceptHeader();
|
|
1203
|
+
else
|
|
1204
|
+
await acceptFrame(controller);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
return failTransform(error);
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
async flush() {
|
|
1214
|
+
if (failed)
|
|
1215
|
+
return;
|
|
1216
|
+
if (!parsedHeader || bufferLength > 0 || !ended) {
|
|
1217
|
+
return failTransform(new RaybaseBackupError("BACKUP_ARCHIVE_TRUNCATED", !parsedHeader
|
|
1218
|
+
? "Backup archive ended inside its clear header"
|
|
1219
|
+
: bufferLength > 0
|
|
1220
|
+
? "Backup archive ended inside an authenticated frame"
|
|
1221
|
+
: "Backup archive is missing its authenticated final frame"));
|
|
1222
|
+
}
|
|
1223
|
+
if (!finalCompletion) {
|
|
1224
|
+
return failTransform(new RaybaseBackupError("BACKUP_ARCHIVE_CORRUPT", "Backup final frame did not produce completion metadata"));
|
|
1225
|
+
}
|
|
1226
|
+
completionState.resolve(finalCompletion);
|
|
1227
|
+
},
|
|
1228
|
+
async cancel(reason) {
|
|
1229
|
+
if (failed)
|
|
1230
|
+
return;
|
|
1231
|
+
failed = true;
|
|
1232
|
+
buffer.fill(0);
|
|
1233
|
+
headerSha256.fill(0);
|
|
1234
|
+
noncePrefix.fill(0);
|
|
1235
|
+
await abortDigests(reason);
|
|
1236
|
+
const error = reason ?? new RaybaseBackupError("BACKUP_ARCHIVE_TRUNCATED", "Backup decryption stream was cancelled");
|
|
1237
|
+
headerState.reject(error);
|
|
1238
|
+
completionState.reject(error);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
return {
|
|
1242
|
+
transform,
|
|
1243
|
+
header: headerState.promise,
|
|
1244
|
+
completion: completionState.promise
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
//# sourceMappingURL=backup-format.js.map
|