From 9c3fd1707b035dac5096c130a3c78203375cf520 Mon Sep 17 00:00:00 2001 From: Sanka Date: Sat, 19 Sep 2026 17:00:51 +0200 Subject: [PATCH 01/41] fix(core): reject malformed Unicode in credential payloads --- packages/core/src/json.ts | 2 +- packages/core/test/credential.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/core/src/json.ts b/packages/core/src/json.ts index 7ea7798..ddb8a23 100644 --- a/packages/core/src/json.ts +++ b/packages/core/src/json.ts @@ -21,7 +21,7 @@ function assertWellFormed(value: string, path: string): void { const codeUnit = value.charCodeAt(index) if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) { const next = value.charCodeAt(index + 1) - if (next < 0xdc00 || next > 0xdfff) { + if (!(next >= 0xdc00 && next <= 0xdfff)) { fail('UNSUPPORTED_JSON_VALUE', 'JSON strings must contain valid Unicode', path) } index += 1 diff --git a/packages/core/test/credential.test.ts b/packages/core/test/credential.test.ts index 78401c7..7f2cd4a 100644 --- a/packages/core/test/credential.test.ts +++ b/packages/core/test/credential.test.ts @@ -59,4 +59,28 @@ describe('credential payload', () => { verifyCredentialPayload({ status: 'retrieved', content: encoded.bytes }, uri, context), ).toBe('valid') }) + + it.each([{ courseId: '\ud800', passed: true }, { '\ud800': 'xcs-101' }])( + 'reports malformed Unicode in payload values or keys as invalid', + (claims) => { + const content = JSON.stringify({ + claims, + issuer: context.issuer, + schema: context.schemaUid, + subject: context.subject, + xcsVersion: '0.1', + }) + const uri = createIpfsPayloadUri(content) + + expect(verifyCredentialPayload({ status: 'retrieved', content }, uri, context)).toBe( + 'invalid', + ) + }, + ) + + it('preserves valid Unicode including surrogate pairs', () => { + const encoded = encodeCredentialPayload({ courseId: 'Cours réussi 🎓', passed: true }, context) + + expect(parseCredentialPayload(encoded.bytes, context).claims.courseId).toBe('Cours réussi 🎓') + }) }) From 6c4fd2973c086ca165ccda00c6211c571980aa95 Mon Sep 17 00:00:00 2001 From: Sanka Date: Sat, 19 Sep 2026 17:01:10 +0200 Subject: [PATCH 02/41] fix(sdk): retain unresolved transactions for reconciliation --- packages/sdk/README.md | 9 ++++++ packages/sdk/src/submission.ts | 15 +++------- packages/sdk/test/submission.test.ts | 43 +++++++++++++++++++++++----- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 282fa3f..df49188 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -33,6 +33,15 @@ seeds, or private keys. Browser and service hosts can persist a validated signed that hook rejects, the SDK keeps the journal stage `signed` so the host's persisted artifact remains recoverable instead of being mislabeled as a terminal signing failure. +`getTransactionStatus` returns `not_found`, not `expired`, when the node cannot find a hash. +An open ledger beyond `LastLedgerSequence` does not prove that the transaction failed: it may +still validate in the preceding ledger or be missing from this node's history. Polling keeps +reconciling until a validated result or the timeout; a timeout remains `pending`. The current +journal does not store the submission-window start needed to prove final absence, so it does +not automatically release an unresolved operation's business lock. `LastLedgerSequence` still +prevents XRPL from validating the signed blob in a later ledger. See +[XRPL reliable submission](https://xrpl.org/docs/concepts/transactions/reliable-transaction-submission). + ## Network safety `connectAndValidateNetwork` checks the connected network ID, required amendment, and profile diff --git a/packages/sdk/src/submission.ts b/packages/sdk/src/submission.ts index 0019d2f..03a9043 100644 --- a/packages/sdk/src/submission.ts +++ b/packages/sdk/src/submission.ts @@ -392,17 +392,10 @@ export async function getTransactionStatus( } } - if (lastLedgerSequence !== undefined) { - const currentLedger = await getCurrentLedgerIndex(client) - if (currentLedger > lastLedgerSequence) { - return { - status: 'expired', - txHash: txHash.toUpperCase(), - lastLedgerSequence, - } - } - } - + // A missing hash is not proof of expiry: the open ledger can be ahead of + // validation, and the server may lack historical ledgers. This API has no + // persisted submission-window start with which to prove complete absence. + // Keep the operation reconcilable instead of recording a terminal failure. return { status: 'not_found', txHash: txHash.toUpperCase(), lastLedgerSequence } } diff --git a/packages/sdk/test/submission.test.ts b/packages/sdk/test/submission.test.ts index 7f3878f..e321c94 100644 --- a/packages/sdk/test/submission.test.ts +++ b/packages/sdk/test/submission.test.ts @@ -312,18 +312,47 @@ describe('reliable submission', () => { expect(request).not.toHaveBeenCalled() }) - it('reports expiration only after the last ledger sequence passes', async () => { - const notFound = Object.assign(new Error('txnNotFound'), { data: { error: 'txnNotFound' } }) + it('does not infer final failure from an open ledger or incomplete history', async () => { + const notFound = Object.assign(new Error('txnNotFound'), { + data: { error: 'txnNotFound', searched_all: false }, + }) const client = mockClient({ - request: vi - .fn() - .mockRejectedValueOnce(notFound) - .mockResolvedValueOnce({ result: { ledger_current_index: 51 } }), + request: vi.fn(async (request: { command: string }) => { + if (request.command === 'tx') throw notFound + return { result: { ledger_current_index: 51 } } + }), }) await expect(getTransactionStatus(client, 'AB'.repeat(32), 50)).resolves.toMatchObject({ - status: 'expired', + status: 'not_found', + }) + }) + + it('keeps polling after txnNotFound so a later validated result is not lost', async () => { + const journal = new MemoryOperationJournal() + let lookups = 0 + const client = mockClient({ + request: vi.fn(async ({ command }: { command: string }) => { + if (command !== 'tx') return { result: { ledger_current_index: 51 } } + if (lookups++ === 0) { + throw Object.assign(new Error('txnNotFound'), { data: { error: 'txnNotFound' } }) + } + return { + result: { + validated: true, + ledger_index: 50, + meta: { TransactionResult: 'tesSUCCESS' }, + }, + } + }), + }) + const result = await submitSignedTransaction(client, signedBlob(), { + journal, + pollIntervalMs: 1, + timeoutMs: 100, }) + expect(result).toMatchObject({ status: 'validated', ledgerIndex: 50 }) + expect(journal.entries.map((entry) => entry.stage)).not.toContain('expired') }) it('rejects an expired prepared transaction before any submit side effect', async () => { From 2af682650c8d3a3a1ddaf0fd2ad36c5ae38e8cbe Mon Sep 17 00:00:00 2001 From: Sanka Date: Sat, 19 Sep 2026 17:02:25 +0200 Subject: [PATCH 03/41] fix(web): release wallet state when operation cleanup fails --- apps/web/README.md | 5 ++ apps/web/app/composables/useWallet.ts | 32 ++++----- apps/web/app/utils/walletOperationCleanup.ts | 30 ++++++++ apps/web/test/walletOperationCleanup.test.ts | 73 ++++++++++++++++++++ 4 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 apps/web/app/utils/walletOperationCleanup.ts create mode 100644 apps/web/test/walletOperationCleanup.test.ts diff --git a/apps/web/README.md b/apps/web/README.md index 804b7e4..00e1f42 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -399,6 +399,11 @@ without creating an XRPL side effect. If it is still unvalidated, retransmission readiness proof and a second ownership check on the business lock. It never asks the wallet to sign again. +RPC teardown and history-refresh failures do not replace the transaction result or leave +the UI busy. Unknown transaction hashes remain recoverable; the SDK no longer records +terminal expiry from the open ledger number alone. Proving final absence requires a +complete validated submission window, which the current journal does not retain. + Native `CredentialAccept` and `CredentialDelete` transactions contain only issuer, subject and credential type; they cannot cryptographically bind an XCS generation ID. An issuer could therefore delete and recreate the same tuple after the final generation check but before ledger execution. The diff --git a/apps/web/app/composables/useWallet.ts b/apps/web/app/composables/useWallet.ts index f29c7c9..8e938de 100644 --- a/apps/web/app/composables/useWallet.ts +++ b/apps/web/app/composables/useWallet.ts @@ -31,6 +31,7 @@ import { } from '~/utils/operationJournal' import { assertCredentialGenerationCurrent } from '~/utils/credentialReview' import { assertPublicRpcUrl } from '~/utils/publicRpcUrl' +import { closeWalletRpc, finishWalletOperation } from '~/utils/walletOperationCleanup' import { assertTransactionSigner } from '~/utils/transactions' import { assertWalletSupportsXcsTransaction, @@ -332,7 +333,7 @@ export function useWallet() { preparedWalletSessions.set(prepared.transaction, walletSession) return prepared.transaction } finally { - if (client.isConnected()) await client.disconnect() + await closeWalletRpc(client) } } @@ -367,11 +368,11 @@ export function useWallet() { walletBusy.value = true walletError.value = null - const operationId = crypto.randomUUID() - const operationStore = operationJournal() - const client = $xrplClientFactory(assertPublicRpcUrl(config.public.rpcUrl)) - + let client: ReturnType | undefined try { + const operationId = crypto.randomUUID() + const operationStore = operationJournal() + client = $xrplClientFactory(assertPublicRpcUrl(config.public.rpcUrl)) // Network identity is known before the wallet is asked to sign. The SDK // will refuse to sign or submit through an unvalidated client. await connectAndValidateNetwork(client, activeProfile) @@ -460,9 +461,7 @@ export function useWallet() { preparedWalletSessions.delete(transaction) return { ...result, ...businessResult } } finally { - await loadOperations().catch(() => undefined) - if (client.isConnected()) await client.disconnect() - walletBusy.value = false + await finishWalletOperation(walletBusy, loadOperations, client) } } @@ -475,9 +474,10 @@ export function useWallet() { async function retryOperation(operationId: string): Promise { walletBusy.value = true walletError.value = null - const operationStore = operationJournal() - const client = $xrplClientFactory(assertPublicRpcUrl(config.public.rpcUrl)) + let client: ReturnType | undefined try { + const operationStore = operationJournal() + client = $xrplClientFactory(assertPublicRpcUrl(config.public.rpcUrl)) const stored = (await operationStore.list()).find( (operation) => operation.operationId === operationId, ) @@ -552,17 +552,15 @@ export function useWallet() { ) return { ...result, ...businessResult } } finally { - await loadOperations().catch(() => undefined) - if (client.isConnected()) await client.disconnect() - walletBusy.value = false + await finishWalletOperation(walletBusy, loadOperations, client) } } async function reconfirmOperation(operationId: string) { walletBusy.value = true walletError.value = null - const operationStore = operationJournal() try { + const operationStore = operationJournal() const stored = (await operationStore.list()).find( (operation) => operation.operationId === operationId, ) @@ -591,8 +589,7 @@ export function useWallet() { operationStore.setBusinessConfirmation(operationId, confirmation, at, evidence), }) } finally { - await loadOperations().catch(() => undefined) - walletBusy.value = false + await finishWalletOperation(walletBusy, loadOperations) } } @@ -602,8 +599,7 @@ export function useWallet() { try { await operationJournal().abandon(operationId) } finally { - await loadOperations().catch(() => undefined) - walletBusy.value = false + await finishWalletOperation(walletBusy, loadOperations) } } diff --git a/apps/web/app/utils/walletOperationCleanup.ts b/apps/web/app/utils/walletOperationCleanup.ts new file mode 100644 index 0000000..4c370ed --- /dev/null +++ b/apps/web/app/utils/walletOperationCleanup.ts @@ -0,0 +1,30 @@ +interface DisconnectableClient { + isConnected(): boolean + disconnect(): Promise +} + +export async function closeWalletRpc(client?: DisconnectableClient): Promise { + try { + if (client?.isConnected()) await client.disconnect() + } catch { + console.warn('Could not close the wallet operation RPC connection.') + } +} + +/** Cleanup must not replace a transaction result or leave the UI locked. */ +export async function finishWalletOperation( + busy: { value: boolean }, + refreshOperations: () => Promise, + client?: DisconnectableClient, +): Promise { + try { + try { + await refreshOperations() + } catch { + console.warn('Could not refresh the wallet operation history.') + } + await closeWalletRpc(client) + } finally { + busy.value = false + } +} diff --git a/apps/web/test/walletOperationCleanup.test.ts b/apps/web/test/walletOperationCleanup.test.ts new file mode 100644 index 0000000..cbe7709 --- /dev/null +++ b/apps/web/test/walletOperationCleanup.test.ts @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { finishWalletOperation } from '../app/utils/walletOperationCleanup' + +afterEach(() => vi.restoreAllMocks()) + +describe('wallet operation cleanup', () => { + it('releases busy state when setup failed before creating an RPC client', async () => { + const busy = { value: true } + const refresh = vi.fn().mockResolvedValue([]) + await finishWalletOperation(busy, refresh) + expect(refresh).toHaveBeenCalledOnce() + expect(busy.value).toBe(false) + }) + + it.each(['history', 'disconnect', 'both'] as const)( + 'keeps the validated result and unlocks the UI despite a %s cleanup failure', + async (failure) => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) + const busy = { value: true } + const refresh = vi.fn(async () => { + if (failure !== 'disconnect') throw new Error('history unavailable') + }) + const client = { + isConnected: () => true, + disconnect: vi.fn(async () => { + if (failure !== 'history') throw new Error('connection close failed') + }), + } + const result = { status: 'validated', transactionResult: 'tesSUCCESS' } + const operation = async () => { + try { + return result + } finally { + await finishWalletOperation(busy, refresh, client) + } + } + await expect(operation()).resolves.toBe(result) + expect(client.disconnect).toHaveBeenCalledOnce() + expect(busy.value).toBe(false) + }, + ) + + it('preserves the original operation error when cleanup also fails', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) + const busy = { value: true } + const original = new Error('wallet approval rejected') + const operation = async () => { + try { + throw original + } finally { + await finishWalletOperation(busy, async () => {}, { + isConnected: () => true, + disconnect: async () => { + throw new Error('connection close failed') + }, + }) + } + } + await expect(operation()).rejects.toBe(original) + expect(busy.value).toBe(false) + }) + + it('does not disconnect an already closed connection', async () => { + const busy = { value: true } + const disconnect = vi.fn() + await finishWalletOperation(busy, async () => {}, { + isConnected: () => false, + disconnect, + }) + expect(disconnect).not.toHaveBeenCalled() + expect(busy.value).toBe(false) + }) +}) From d9eeff31ac1cfed5a4b99c1f02624cdb1b4af64a Mon Sep 17 00:00:00 2001 From: Sanka Date: Wed, 23 Sep 2026 16:22:54 +0200 Subject: [PATCH 04/41] chore(snapshot): preserve local protocol and indexer fixes --- apps/indexer/src/xrpl-source.ts | 10 +++- apps/indexer/test/xrpl-source.test.ts | 35 +++++++++++++ docs/runbooks/indexer.md | 6 +++ packages/core/src/index.ts | 1 + packages/sdk/src/submission.ts | 72 ++++++++++++++++++--------- 5 files changed, 99 insertions(+), 25 deletions(-) diff --git a/apps/indexer/src/xrpl-source.ts b/apps/indexer/src/xrpl-source.ts index 6f35527..49039f3 100644 --- a/apps/indexer/src/xrpl-source.ts +++ b/apps/indexer/src/xrpl-source.ts @@ -97,10 +97,18 @@ function normalizeTransaction(value: unknown): LedgerTransaction { delete cleanTransaction.metaData delete cleanTransaction.hash + // Nodes add these API conveniences at request time; they are not hashed ledger metadata. + const cleanMetadata = { ...metadata } + delete cleanMetadata.nftoken_id + delete cleanMetadata.nftoken_ids + delete cleanMetadata.offer_id + delete cleanMetadata.mpt_issuance_id + delete cleanMetadata.delivered_amount + return { hash: transactionHash, transaction: cleanTransaction, - metadata: { ...metadata }, + metadata: cleanMetadata, transactionIndex, } } diff --git a/apps/indexer/test/xrpl-source.test.ts b/apps/indexer/test/xrpl-source.test.ts index 1a3e18e..012e76d 100644 --- a/apps/indexer/test/xrpl-source.test.ts +++ b/apps/indexer/test/xrpl-source.test.ts @@ -102,6 +102,41 @@ describe('normalizeLedgerResponse', () => { }) }) + it.each<[string, unknown]>([ + ['nftoken_id', FIRST_TX_HASH], + ['nftoken_ids', [FIRST_TX_HASH]], + ['offer_id', FIRST_TX_HASH], + ['mpt_issuance_id', '1'.repeat(48)], + ['delivered_amount', '1'], + ])( + 'ignores the synthetic API metadata field %s without mutating the response', + (field, value) => { + const decorated = response() + const metadata = record(transactions(decorated)[0]!.meta) + metadata[field] = value + + expect(normalizeLedgerResponse(decorated)).toEqual(normalizeLedgerResponse(response())) + expect(metadata[field]).toEqual(value) + }, + ) + + it.each<[string, unknown]>([ + ['DeliveredAmount', '1'], + ['AffectedNodes', [{ ModifiedNode: { LedgerEntryType: 'AccountRoot' } }]], + ['TransactionResult', 'tecFAILED'], + ['FutureCanonicalField', 'preserved'], + ])('preserves %s for the quorum to detect canonical metadata differences', (field, value) => { + const changed = response() + const metadata = record(transactions(changed)[0]!.meta) + metadata[field] = value + metadata.delivered_amount = '1' + + const normalized = normalizeLedgerResponse(changed) + expect(normalized).not.toEqual(normalizeLedgerResponse(response())) + expect(normalized.transactions[1]!.metadata[field]).toEqual(value) + expect(normalized.transactions[1]!.metadata).not.toHaveProperty('delivered_amount') + }) + it('rejects a ledger that is not marked validated or closed', () => { const unvalidated = response() unvalidated.validated = false diff --git a/docs/runbooks/indexer.md b/docs/runbooks/indexer.md index 583ba70..1e45444 100644 --- a/docs/runbooks/indexer.md +++ b/docs/runbooks/indexer.md @@ -59,6 +59,12 @@ new empty database; never update this profile in place. ## Healthy state +Quorum comparison excludes the documented synthetic API metadata fields `nftoken_id`, +`nftoken_ids`, `offer_id`, `mpt_issuance_id` and lowercase `delivered_amount`. +Nodes can add these at request time without changing ledger contents. Canonical metadata, +including `AffectedNodes` and uppercase `DeliveredAmount`, remains subject to strict comparison. +Do not bypass a divergence by comparing ledger hashes alone or resetting the checkpoint. + Readiness requires matching normalized ledgers from both sources, a live fenced writer lease, the configured amendment and activation hash, no ledger gap, and a fresh checkpoint whose index/hash exactly matches the writer's agreed state and includes a transaction root. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 28eaade..193466d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,6 +29,7 @@ export { createHttpsPayloadUri, createIpfsPayloadUri, parsePayloadUri, + payloadDigest, verifyPayloadIntegrity, type HttpsPayloadUri, type IpfsPayloadUri, diff --git a/packages/sdk/src/submission.ts b/packages/sdk/src/submission.ts index 03a9043..3550f53 100644 --- a/packages/sdk/src/submission.ts +++ b/packages/sdk/src/submission.ts @@ -64,6 +64,14 @@ export interface ValidatedSignature { readonly lastLedgerSequence: number } +export interface ValidatedSignedTransaction { + /** The exact unsigned fields recovered from the signed blob. */ + readonly transaction: Readonly + readonly txBlob: string + readonly txHash: string + readonly lastLedgerSequence: number +} + export interface ReliableSubmissionOptions { readonly journal: OperationJournal readonly operationId?: string | undefined @@ -106,6 +114,43 @@ const HASH_PATTERN = /^[0-9a-fA-F]{64}$/u const BLOB_PATTERN = /^(?:[0-9a-fA-F]{2})+$/u const XRPL_JS_5_MAX_URI_HEX_CHARACTERS = 256 +/** + * Decode and cryptographically verify a single-signed XRPL transaction blob. + * This is the server-safe counterpart to the browser submission validation. + */ +export function decodeSignedTransactionBlob(txBlob: string): ValidatedSignedTransaction { + if (!BLOB_PATTERN.test(txBlob)) { + throw new XcsSdkError( + 'XCS_SDK_INVALID_SIGNED_BLOB', + 'Signed transaction input must be non-empty hexadecimal data.', + ) + } + + let decoded: Record + let txHash: string + try { + decoded = decode(txBlob) + txHash = hashes.hashSignedTx(txBlob).toUpperCase() + } catch { + throw new XcsSdkError('XCS_SDK_INVALID_SIGNED_BLOB', 'Cannot decode signed transaction.') + } + assertValidSingleSignature(decoded, txBlob) + const lastLedgerSequence = asPositiveInteger(decoded.LastLedgerSequence) + if (lastLedgerSequence === undefined) { + throw new XcsSdkError( + 'XCS_SDK_INVALID_SIGNED_BLOB', + 'Signed transaction must contain a positive LastLedgerSequence.', + ) + } + + return { + transaction: withoutSignatureFields(decoded) as unknown as SubmittableTransaction, + txBlob, + txHash, + lastLedgerSequence, + } +} + export async function autofillXcsTransaction( client: Client, transaction: T, @@ -246,31 +291,10 @@ export async function submitSignedTransaction( 'Connect and validate the XRPL client before submitting a transaction.', ) } - if (!BLOB_PATTERN.test(txBlob)) { - throw new XcsSdkError( - 'XCS_SDK_INVALID_SIGNED_BLOB', - 'Signed transaction input must be non-empty hexadecimal data.', - ) - } - const operationId = options.operationId ?? crypto.randomUUID() - let decoded: Record - let txHash: string - try { - decoded = decode(txBlob) - txHash = hashes.hashSignedTx(txBlob).toUpperCase() - } catch { - throw new XcsSdkError('XCS_SDK_INVALID_SIGNED_BLOB', 'Cannot decode signed transaction.') - } - assertValidSingleSignature(decoded, txBlob) - - const lastLedgerSequence = asPositiveInteger(decoded.LastLedgerSequence) - if (lastLedgerSequence === undefined) { - throw new XcsSdkError( - 'XCS_SDK_INVALID_SIGNED_BLOB', - 'Reliable submission requires a signed transaction with LastLedgerSequence.', - ) - } + const validated = decodeSignedTransactionBlob(txBlob) + const decoded = validated.transaction as unknown as Record + const { txHash, lastLedgerSequence } = validated // Persist the recovery identifiers before the first network side effect. await append(options.journal, { From db00aeed4378854dd7085e63f663a049f1fcc82d Mon Sep 17 00:00:00 2001 From: Sanka Date: Wed, 23 Sep 2026 16:22:54 +0200 Subject: [PATCH 05/41] chore(snapshot): preserve recovered hosted payload API and migrations --- apps/api/package.json | 1 + apps/api/src/app.ts | 78 + apps/api/src/config.ts | 33 + apps/api/src/hosted-payload-resolver.ts | 40 + apps/api/src/hosted-payloads-repository.ts | 109 + apps/api/src/hosted-payloads.ts | 283 +++ apps/api/src/index.ts | 3 + apps/api/src/main.ts | 22 +- apps/api/test/config.test.ts | 31 + apps/api/test/hosted-payloads.test.ts | 291 +++ apps/api/test/postgres.integration.test.ts | 58 + docs/runbooks/local-source-recovery.md | 109 + packages/db/drizzle/0001_hosted_payloads.sql | 33 + ...2_hosted_payload_locator_compatibility.sql | 3 + packages/db/drizzle/meta/0001_snapshot.json | 2110 ++++++++++++++++ packages/db/drizzle/meta/0002_snapshot.json | 2203 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 14 + packages/db/src/provision.ts | 1 + packages/db/src/schema/index.ts | 1 + packages/db/src/schema/payloads.ts | 51 + packages/db/test/schema.test.ts | 20 +- 21 files changed, 5489 insertions(+), 5 deletions(-) create mode 100644 apps/api/src/hosted-payload-resolver.ts create mode 100644 apps/api/src/hosted-payloads-repository.ts create mode 100644 apps/api/src/hosted-payloads.ts create mode 100644 apps/api/test/hosted-payloads.test.ts create mode 100644 docs/runbooks/local-source-recovery.md create mode 100644 packages/db/drizzle/0001_hosted_payloads.sql create mode 100644 packages/db/drizzle/0002_hosted_payload_locator_compatibility.sql create mode 100644 packages/db/drizzle/meta/0001_snapshot.json create mode 100644 packages/db/drizzle/meta/0002_snapshot.json create mode 100644 packages/db/src/schema/payloads.ts diff --git a/apps/api/package.json b/apps/api/package.json index 30a799c..8f5b116 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -27,6 +27,7 @@ "@fastify/rate-limit": "^10.3.0", "@xcs-protocol/core": "workspace:*", "@xcs-protocol/db": "workspace:*", + "@xcs-protocol/sdk": "workspace:*", "drizzle-orm": "^0.45.2", "fastify": "^5.5.0", "ipaddr.js": "^2.2.0", diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index e8c7764..d3b2e7b 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -32,6 +32,11 @@ import { encodeSchemaRegistrationCursor, } from './pagination.js' import { DemoPinningService, PinningError } from './pinning.js' +import { + HostedPayloadError, + HostedPayloadService, + HOSTED_PAYLOAD_LOCATOR_PATTERN, +} from './hosted-payloads.js' import { authoritativeSchemaCatalogBundle, MAX_SCHEMA_CATALOG_ENTRIES } from './schema-catalog.js' import { authoritativeResolvedSchema, @@ -128,6 +133,7 @@ export interface CreateApiOptions { trustedProxyCidrs?: string[] verifyRateLimit?: number pinningService?: DemoPinningService + hostedPayloadService?: HostedPayloadService operationalMetrics?: { token: string repository: OperationalMetricsRepository @@ -305,6 +311,10 @@ export async function createApi(options: CreateApiOptions): Promise { + if (error instanceof HostedPayloadError) { + reply.code(error.statusCode).send({ error: error.code, message: error.code }) + return + } if (error instanceof PinningError) { reply.code(error.statusCode).send({ error: error.code, message: error.code }) return @@ -346,6 +356,74 @@ export async function createApi(options: CreateApiOptions): Promise( + '/p/:locator', + { + schema: { params }, + config: { rateLimit: { max: 300, timeWindow: '1 minute' } }, + }, + async (request, reply) => { + const payload = await service.get(request.params.locator) + reply.header('cache-control', 'public, max-age=31536000, immutable') + reply.header('content-type', 'application/json; charset=utf-8') + reply.header('etag', `"${payload.digestHex}"`) + reply.header('x-content-type-options', 'nosniff') + return reply.send(payload.content) + }, + ) + app.post<{ + Params: { locator: string } + Body: { network: string; payloadBase64: string; signedTransactionBlob: string } + }>( + '/v1/payloads/:locator', + { + schema: { + params, + body: { + type: 'object', + additionalProperties: false, + required: ['network', 'payloadBase64', 'signedTransactionBlob'], + properties: { + network: { type: 'string', pattern: PROFILE_PATTERN }, + payloadBase64: { type: 'string', minLength: 4, maxLength: 90_000 }, + signedTransactionBlob: { + type: 'string', + pattern: '^(?:[0-9A-Fa-f]{2})+$', + maxLength: 32768, + }, + }, + }, + response: { + 400: errorResponseSchema, + 401: errorResponseSchema, + 403: errorResponseSchema, + 404: errorResponseSchema, + 409: errorResponseSchema, + 413: errorResponseSchema, + 429: errorResponseSchema, + 503: errorResponseSchema, + }, + }, + config: { rateLimit: { max: 20, timeWindow: '1 minute' } }, + }, + async (request) => + service.publish({ + network: request.body.network, + locator: request.params.locator, + payloadBase64: request.body.payloadBase64, + signedTransactionBlob: request.body.signedTransactionBlob, + ipAddress: request.ip, + }), + ) + } app.get('/health/ready', { config: { rateLimit: false } }, async (_request, reply) => { reply.header('cache-control', 'no-store') try { diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index 5dd8089..f42fc8e 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -12,6 +12,9 @@ export interface ApiConfig { untrustedIssuers: string[] allowedOrigins: string[] payloadFetchEnabled: boolean + hostedPayloads: + | { enabled: false } + | { enabled: true; publicBaseUrl: string; ipHashSecret: string; networks: string[] } readinessMaxLedgerAgeSeconds: number operationalMetrics: | { enabled: false } @@ -138,6 +141,22 @@ function origins(value: string | undefined): string[] { }) } +function hostedPayloadBaseUrl(value: string): string { + const parsed = new URL(value) + if ( + parsed.protocol !== 'https:' || + parsed.username !== '' || + parsed.password !== '' || + parsed.origin !== value || + Buffer.byteLength(`${value}/p/${'0'.repeat(18)}#xcs-sha256=${'0'.repeat(64)}`, 'utf8') > 128 + ) { + throw new Error( + 'XCS_PUBLIC_PAYLOAD_BASE_URL must be a short HTTPS origin that produces a URI of at most 128 bytes', + ) + } + return value +} + export function loadApiConfig(environment: NodeJS.ProcessEnv = process.env): ApiConfig { const configuredInternalSsrToken = internalSsrToken(environment) const port = Number(environment.XCS_API_PORT ?? environment.API_PORT ?? '3001') @@ -168,6 +187,19 @@ export function loadApiConfig(environment: NodeJS.ProcessEnv = process.env): Api } : ({ enabled: false } as const) const trustedIssuers = addressList(environment.XCS_TRUSTED_ISSUERS, 'XCS_TRUSTED_ISSUERS') + const hostedPayloadsEnabled = strictBoolean( + environment.XCS_HOSTED_PAYLOADS_ENABLED, + false, + 'XCS_HOSTED_PAYLOADS_ENABLED', + ) + const hostedPayloads: ApiConfig['hostedPayloads'] = hostedPayloadsEnabled + ? { + enabled: true, + publicBaseUrl: hostedPayloadBaseUrl(required(environment, 'XCS_PUBLIC_PAYLOAD_BASE_URL')), + ipHashSecret: required(environment, 'XCS_PAYLOAD_STORAGE_IP_HASH_SECRET'), + networks: list(environment.XCS_HOSTED_PAYLOAD_NETWORKS), + } + : { enabled: false } const untrustedIssuers = addressList(environment.XCS_UNTRUSTED_ISSUERS, 'XCS_UNTRUSTED_ISSUERS') if (trustedIssuers.some((issuer) => untrustedIssuers.includes(issuer))) { throw new Error('XCS_TRUSTED_ISSUERS and XCS_UNTRUSTED_ISSUERS must not overlap') @@ -189,6 +221,7 @@ export function loadApiConfig(environment: NodeJS.ProcessEnv = process.env): Api 'XCS_PAYLOAD_FETCH_ENABLED', ), readinessMaxLedgerAgeSeconds, + hostedPayloads, operationalMetrics: operationalMetrics(environment, configuredInternalSsrToken), demoPinning, } diff --git a/apps/api/src/hosted-payload-resolver.ts b/apps/api/src/hosted-payload-resolver.ts new file mode 100644 index 0000000..809ccbe --- /dev/null +++ b/apps/api/src/hosted-payload-resolver.ts @@ -0,0 +1,40 @@ +import { HostedPayloadService, HostedPayloadError } from './hosted-payloads.js' +import { PayloadUnavailableError, PayloadInvalidError } from './payload-resolver.js' +import type { PayloadResolver } from './types.js' +import { parsePayloadUri } from '@xcs-protocol/core' +export class HostedPayloadResolver implements PayloadResolver { + constructor( + private readonly origin: string, + private readonly hosted: HostedPayloadService, + private readonly external: PayloadResolver, + ) {} + async resolve(uri: string) { + const parsed = parsePayloadUri(uri) + if (parsed.kind !== 'https') return this.external.resolve(uri) + const url = new URL(parsed.fetchUrl) + const match = /^\/p\/([0-9a-f]{18}|[0-9a-f]{20})$/u.exec(url.pathname) + if ( + url.origin !== this.origin || + url.username !== '' || + url.password !== '' || + url.search !== '' || + match === null + ) { + return this.external.resolve(uri) + } + try { + const payload = await this.hosted.get(match[1]!) + return new TextEncoder().encode(payload.content) + } catch (error) { + if (error instanceof HostedPayloadError) { + if (error.code === 'PAYLOAD_NOT_FOUND') { + throw new PayloadUnavailableError('Hosted payload not found', { cause: error }) + } + if (error.code === 'PAYLOAD_STORAGE_INTEGRITY_ERROR') { + throw new PayloadInvalidError('Hosted payload storage integrity failed', { cause: error }) + } + } + throw error + } + } +} diff --git a/apps/api/src/hosted-payloads-repository.ts b/apps/api/src/hosted-payloads-repository.ts new file mode 100644 index 0000000..00f3a1b --- /dev/null +++ b/apps/api/src/hosted-payloads-repository.ts @@ -0,0 +1,109 @@ +import type { DatabaseClient } from '@xcs-protocol/db' +import { HostedPayloadError, type HostedPayloadRepository } from './hosted-payloads.js' +import { + hostedPayloadPublications, + hostedPayloads, + runSerializableTransaction, +} from '@xcs-protocol/db' +import { and, count, eq, gte, or } from 'drizzle-orm' +export class PostgresHostedPayloadRepository implements HostedPayloadRepository { + constructor(private readonly db: DatabaseClient['db']) {} + async get(locator: string) { + const [row] = await this.db + .select({ + locator: hostedPayloads.locator, + digestHex: hostedPayloads.digestHex, + content: hostedPayloads.content, + }) + .from(hostedPayloads) + .where(eq(hostedPayloads.locator, locator)) + .limit(1) + return row + } + async publish(input: Parameters[0]) { + return runSerializableTransaction(this.db, async (tx) => { + const [existingPublication] = await tx + .select() + .from(hostedPayloadPublications) + .where(eq(hostedPayloadPublications.transactionHash, input.transactionHash)) + .limit(1) + if (existingPublication !== void 0) { + if ( + existingPublication.locator !== input.locator || + existingPublication.profileId !== input.profileId || + existingPublication.issuer !== input.issuer || + existingPublication.subject !== input.subject || + existingPublication.schemaUid !== input.schemaUid + ) { + throw new HostedPayloadError('PAYLOAD_PUBLICATION_CONFLICT', 409) + } + const [existingPayload] = await tx + .select({ + locator: hostedPayloads.locator, + digestHex: hostedPayloads.digestHex, + content: hostedPayloads.content, + }) + .from(hostedPayloads) + .where(eq(hostedPayloads.locator, input.locator)) + .limit(1) + if ( + existingPayload === void 0 || + existingPayload.digestHex !== input.digestHex || + existingPayload.content !== input.content + ) { + throw new HostedPayloadError('PAYLOAD_STORAGE_INTEGRITY_ERROR', 500) + } + return existingPayload + } + const since = new Date(input.now.getTime() - 24 * 60 * 60 * 1e3) + const [quota] = await tx + .select({ value: count() }) + .from(hostedPayloadPublications) + .where( + and( + gte(hostedPayloadPublications.createdAt, since), + or( + eq(hostedPayloadPublications.issuer, input.issuer), + eq(hostedPayloadPublications.requesterIpHash, input.requesterIpHash), + ), + ), + ) + if ((quota?.value ?? 0) >= input.dailyLimit) { + throw new HostedPayloadError('PAYLOAD_PUBLICATION_QUOTA_EXCEEDED', 429) + } + await tx + .insert(hostedPayloads) + .values({ + locator: input.locator, + digestHex: input.digestHex, + content: input.content, + createdAt: input.now, + }) + .onConflictDoNothing() + const [payload] = await tx + .select({ + locator: hostedPayloads.locator, + digestHex: hostedPayloads.digestHex, + content: hostedPayloads.content, + }) + .from(hostedPayloads) + .where(eq(hostedPayloads.locator, input.locator)) + .limit(1) + if (payload === void 0) throw new HostedPayloadError('PAYLOAD_STORAGE_UNAVAILABLE', 503) + if (payload.digestHex !== input.digestHex || payload.content !== input.content) { + throw new HostedPayloadError('PAYLOAD_LOCATOR_COLLISION', 409) + } + await tx.insert(hostedPayloadPublications).values({ + transactionHash: input.transactionHash, + locator: input.locator, + profileId: input.profileId, + issuer: input.issuer, + subject: input.subject, + schemaUid: input.schemaUid, + requesterIpHash: input.requesterIpHash, + createdAt: input.now, + }) + return payload + }) + } +} diff --git a/apps/api/src/hosted-payloads.ts b/apps/api/src/hosted-payloads.ts new file mode 100644 index 0000000..c105533 --- /dev/null +++ b/apps/api/src/hosted-payloads.ts @@ -0,0 +1,283 @@ +import { createHmac } from 'node:crypto' +import { + parseCredentialPayload, + parseJson, + parsePayloadUri, + payloadDigest, +} from '@xcs-protocol/core' +import { + assertXcsTransactionSemantics, + credentialHexToUri, + decodeSignedTransactionBlob, +} from '@xcs-protocol/sdk' +import { assertAuthoritativeLedgerEvidence } from './indexer-status.js' +import { DEFAULT_LEDGER_MAX_AGE_SECONDS } from './ledger-freshness.js' +import { authoritativeResolvedSchema, schemaProjectionEvidenceUids } from './schema-projection.js' +import type { ApiRepository } from './types.js' + +export interface HostedPayloadRecord { + readonly locator: string + readonly digestHex: string + readonly content: string +} +export interface HostedPayloadRepository { + publish(input: { + locator: string + digestHex: string + content: string + transactionHash: string + profileId: string + issuer: string + subject: string + schemaUid: string + requesterIpHash: string + now: Date + dailyLimit: number + }): Promise + get(locator: string): Promise +} +export interface HostedPayloadServiceOptions { + readonly repository: HostedPayloadRepository + readonly apiRepository: ApiRepository + readonly publicBaseUrl: string + readonly ipHashSecret: string + readonly enabledNetworks: ReadonlySet + readonly maxLedgerAgeSeconds?: number + readonly now?: () => Date +} +export const HOSTED_PAYLOAD_MAX_BYTES = 64 * 1024 +export const HOSTED_PAYLOAD_LOCATOR_HEX_LENGTH = 18 +export const HOSTED_PAYLOAD_LOCATOR_PATTERN = '^(?:[0-9a-f]{18}|[0-9a-f]{20})$' +const validLocator = new RegExp(HOSTED_PAYLOAD_LOCATOR_PATTERN, 'u') +const DAILY_PUBLICATION_LIMIT = 50 +export class HostedPayloadError extends Error { + constructor( + readonly code: string, + readonly statusCode: number, + options: { cause?: unknown } = {}, + ) { + super(code, options) + this.name = 'HostedPayloadError' + } +} +export function hostedPayloadLocator(digestHex: string) { + if (!/^[0-9a-f]{64}$/u.test(digestHex)) { + throw new HostedPayloadError('PAYLOAD_DIGEST_INVALID', 400) + } + return digestHex.slice(0, HOSTED_PAYLOAD_LOCATOR_HEX_LENGTH) +} +export function hostedPayloadFetchUrl(publicBaseUrl: string, locator: string) { + if (!validLocator.test(locator)) { + throw new HostedPayloadError('PAYLOAD_LOCATOR_INVALID', 400) + } + return `${publicBaseUrl}/p/${locator}` +} +function decodePayloadBase64(value: string) { + if ( + value.length === 0 || + value.length % 4 !== 0 || + !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value) + ) { + throw new HostedPayloadError('PAYLOAD_BASE64_INVALID', 400) + } + const content = Uint8Array.from(Buffer.from(value, 'base64')) + if (content.length === 0 || content.length > HOSTED_PAYLOAD_MAX_BYTES) { + throw new HostedPayloadError('PAYLOAD_SIZE_INVALID', 413) + } + return content +} +function hashIp(ipAddress: string, secret: string) { + return createHmac('sha256', secret).update(ipAddress, 'utf8').digest('hex') +} +export class HostedPayloadService { + constructor(private readonly options: HostedPayloadServiceOptions) { + if (Buffer.byteLength(options.ipHashSecret, 'utf8') < 32) { + throw new Error('Hosted payload IP hash secret must contain at least 32 bytes') + } + this.now = options.now ?? (() => new Date()) + } + private readonly now: () => Date + async get(locator: string) { + if (!validLocator.test(locator)) { + throw new HostedPayloadError('PAYLOAD_NOT_FOUND', 404) + } + const payload = await this.options.repository.get(locator) + if (payload === undefined) throw new HostedPayloadError('PAYLOAD_NOT_FOUND', 404) + if (payload.locator !== locator || payload.digestHex.slice(0, locator.length) !== locator) { + throw new HostedPayloadError('PAYLOAD_STORAGE_INTEGRITY_ERROR', 500) + } + if (payloadDigest(payload.content) !== payload.digestHex) { + throw new HostedPayloadError('PAYLOAD_STORAGE_INTEGRITY_ERROR', 500) + } + return payload + } + async publish(input: { + network: string + locator: string + payloadBase64: string + signedTransactionBlob: string + ipAddress: string + }) { + if (!this.options.enabledNetworks.has(input.network)) { + throw new HostedPayloadError('PAYLOAD_HOSTING_NETWORK_DISABLED', 404) + } + const bytes = decodePayloadBase64(input.payloadBase64) + const digestHex = payloadDigest(bytes) + if (hostedPayloadLocator(digestHex) !== input.locator) { + throw new HostedPayloadError('PAYLOAD_LOCATOR_MISMATCH', 400) + } + let signed + try { + signed = decodeSignedTransactionBlob(input.signedTransactionBlob) + } catch (error) { + throw new HostedPayloadError('SIGNED_TRANSACTION_INVALID', 401, { cause: error }) + } + const validated = await this.options.apiRepository.withConsistentSnapshot( + async (repository) => { + const network = await repository.getNetwork(input.network) + if (network === undefined) throw new HostedPayloadError('NETWORK_NOT_FOUND', 404) + if (network.networkId !== 1) { + throw new HostedPayloadError('PAYLOAD_HOSTING_NETWORK_DISABLED', 404) + } + let semantics + try { + semantics = assertXcsTransactionSemantics(signed.transaction, { + profileId: network.profileId, + xcsVersion: network.xcsVersion, + networkId: network.networkId, + requiredAmendment: network.requiredAmendment, + registryAddress: network.registryAddress, + registrationAmountDrops: String(network.registrationAmountDrops), + activationLedgerIndex: network.activationLedgerIndex, + activationLedgerHash: network.activationLedgerHash, + }) + } catch (error) { + throw new HostedPayloadError('SIGNED_TRANSACTION_INVALID', 401, { cause: error }) + } + if (semantics.kind !== 'credential-create') { + throw new HostedPayloadError('SIGNED_TRANSACTION_NOT_CREDENTIAL_CREATE', 400) + } + const transaction = semantics.transaction + const expectedFetchUrl = hostedPayloadFetchUrl(this.options.publicBaseUrl, input.locator) + let credentialUri + try { + if (transaction.URI === undefined) throw new Error('signed URI missing') + credentialUri = credentialHexToUri(transaction.URI) + const parsedUri = parsePayloadUri(credentialUri) + if ( + parsedUri.kind !== 'https' || + parsedUri.fetchUrl !== expectedFetchUrl || + parsedUri.digestHex !== digestHex + ) { + throw new Error('signed URI mismatch') + } + } catch (error) { + throw new HostedPayloadError('SIGNED_TRANSACTION_PAYLOAD_MISMATCH', 403, { + cause: error, + }) + } + let header: Record + try { + const parsed = parseJson(bytes) + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + throw new Error('payload is not an object') + } + header = parsed as Record + } catch (error) { + throw new HostedPayloadError('PAYLOAD_INVALID', 400, { cause: error }) + } + if ( + header.issuer !== transaction.Account || + header.subject !== transaction.Subject || + header.schema !== semantics.schemaUid + ) { + throw new HostedPayloadError('SIGNED_TRANSACTION_PAYLOAD_MISMATCH', 403) + } + const databaseNow = await repository.getDatabaseTime() + const status = await repository.getIndexerStatus(input.network) + const checkpoint = await repository.getLatestCheckpoint(input.network) + const authority = { + expectedProfileId: input.network, + status, + checkpoint, + now: databaseNow, + maxLedgerAgeSeconds: this.options.maxLedgerAgeSeconds ?? DEFAULT_LEDGER_MAX_AGE_SECONDS, + minimumLedgerIndex: network.activationLedgerIndex, + } + assertAuthoritativeLedgerEvidence({ ...authority, projectionLedgerIndexes: [] }) + const schemaRow = await repository.getSchema(input.network, semantics.schemaUid) + if (schemaRow === undefined) throw new HostedPayloadError('SCHEMA_NOT_FOUND', 404) + const schemaEvidence = await repository.getSchemaProjectionEvidence({ + profileId: input.network, + schemaUids: schemaProjectionEvidenceUids([schemaRow], input.network), + }) + assertAuthoritativeLedgerEvidence({ + ...authority, + projectionLedgerIndexes: schemaEvidence.map((item) => item.schema.ledgerIndex), + }) + const schema = authoritativeResolvedSchema(schemaRow, schemaEvidence, { + profileId: input.network, + schemaUid: semantics.schemaUid, + networkId: network.networkId, + activationLedgerIndex: network.activationLedgerIndex, + }) + const transactionEvents = await repository.getCredentialEventsByTransaction({ + profileId: input.network, + transactionHash: signed.txHash.toLowerCase(), + issuer: transaction.Account, + subject: transaction.Subject, + schemaUid: semantics.schemaUid, + limit: 2, + }) + if ( + transactionEvents.length !== 1 || + transactionEvents[0]?.eventType !== 'created' || + transactionEvents[0].uriHex?.toLowerCase() !== transaction.URI?.toLowerCase() + ) { + throw new HostedPayloadError('SIGNED_TRANSACTION_NOT_INDEXED', 409) + } + assertAuthoritativeLedgerEvidence({ + ...authority, + projectionLedgerIndexes: [transactionEvents[0].ledgerIndex], + }) + try { + parseCredentialPayload(bytes, { + issuer: transaction.Account, + subject: transaction.Subject, + schemaUid: semantics.schemaUid, + fields: schema.fields, + }) + } catch (error) { + throw new HostedPayloadError('PAYLOAD_INVALID', 400, { cause: error }) + } + return { + transactionHash: signed.txHash.toLowerCase(), + issuer: transaction.Account, + subject: transaction.Subject, + schemaUid: semantics.schemaUid, + content: new TextDecoder('utf-8', { fatal: true }).decode(bytes), + } + }, + ) + const stored = await this.options.repository.publish({ + locator: input.locator, + digestHex, + content: validated.content, + transactionHash: validated.transactionHash, + profileId: input.network, + issuer: validated.issuer, + subject: validated.subject, + schemaUid: validated.schemaUid, + requesterIpHash: hashIp(input.ipAddress, this.options.ipHashSecret), + now: this.now(), + dailyLimit: DAILY_PUBLICATION_LIMIT, + }) + return { + uri: `${hostedPayloadFetchUrl(this.options.publicBaseUrl, stored.locator)}#xcs-sha256=${stored.digestHex}`, + fetchUrl: hostedPayloadFetchUrl(this.options.publicBaseUrl, stored.locator), + digestHex: stored.digestHex, + byteLength: Buffer.byteLength(stored.content, 'utf8'), + transactionHash: validated.transactionHash, + } + } +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index ee9dedf..8746e05 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -12,3 +12,6 @@ export * from './schema-catalog.js' export * from './schema-projection.js' export * from './types.js' export * from './verification.js' +export * from './hosted-payloads.js' +export * from './hosted-payloads-repository.js' +export * from './hosted-payload-resolver.js' diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 05d04d8..84cd2cd 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -2,6 +2,9 @@ import { createDatabaseClient } from '@xcs-protocol/db' import { createApi } from './app.js' import { loadApiConfig } from './config.js' +import { HostedPayloadService } from './hosted-payloads.js' +import { PostgresHostedPayloadRepository } from './hosted-payloads-repository.js' +import { HostedPayloadResolver } from './hosted-payload-resolver.js' import { KuboPinStore } from './kubo.js' import { PostgresOperationalMetricsRepository } from './operational-metrics-repository.js' import { DisabledPayloadResolver, SafePayloadResolver } from './payload-resolver.js' @@ -23,10 +26,26 @@ const pinningService = config.demoPinning.enabled maxLedgerAgeSeconds: config.readinessMaxLedgerAgeSeconds, }) : undefined +const hostedPayloadService = config.hostedPayloads.enabled + ? new HostedPayloadService({ + repository: new PostgresHostedPayloadRepository(database.db), + apiRepository: repository, + publicBaseUrl: config.hostedPayloads.publicBaseUrl, + ipHashSecret: config.hostedPayloads.ipHashSecret, + enabledNetworks: new Set(config.hostedPayloads.networks), + maxLedgerAgeSeconds: config.readinessMaxLedgerAgeSeconds, + }) + : undefined const app = await createApi({ repository, resolver: config.payloadFetchEnabled - ? new SafePayloadResolver(config.ipfsGateway) + ? config.hostedPayloads.enabled && hostedPayloadService !== undefined + ? new HostedPayloadResolver( + config.hostedPayloads.publicBaseUrl, + hostedPayloadService, + new SafePayloadResolver(config.ipfsGateway), + ) + : new SafePayloadResolver(config.ipfsGateway) : new DisabledPayloadResolver(), trustPolicy: new StaticTrustPolicy({ trusted: config.trustedIssuers, @@ -46,6 +65,7 @@ const app = await createApi({ } : {}), ...(pinningService === undefined ? {} : { pinningService }), + ...(hostedPayloadService === undefined ? {} : { hostedPayloadService }), logger: true, }) diff --git a/apps/api/test/config.test.ts b/apps/api/test/config.test.ts index 15b2c95..7b501e3 100644 --- a/apps/api/test/config.test.ts +++ b/apps/api/test/config.test.ts @@ -7,6 +7,37 @@ const INTERNAL_SSR_TOKEN = 'test-internal-ssr-token-000000000001' const METRICS_TOKEN = 'test-operational-metrics-token-00000001' describe('API configuration', () => { + it('enables hosted public payloads only with an explicit short HTTPS origin and configuration', () => { + const env = { + XCS_DATABASE_URL: 'postgres://localhost/xcs', + XCS_INTERNAL_API_TOKEN: INTERNAL_SSR_TOKEN, + XCS_HOSTED_PAYLOADS_ENABLED: 'true', + XCS_PUBLIC_PAYLOAD_BASE_URL: 'https://payload.test', + XCS_PAYLOAD_STORAGE_IP_HASH_SECRET: 'test-storage-ip-hash-secret-0000000', + XCS_HOSTED_PAYLOAD_NETWORKS: 'testnet', + } + expect(loadApiConfig(env).hostedPayloads).toMatchObject({ + enabled: true, + publicBaseUrl: 'https://payload.test', + networks: ['testnet'], + }) + for (const origin of [ + 'http://localhost', + 'https://payload.test/path', + 'https://user:pass@payload.test', + `https://${'a'.repeat(50)}.test`, + ]) { + expect(() => loadApiConfig({ ...env, XCS_PUBLIC_PAYLOAD_BASE_URL: origin })).toThrow( + 'short HTTPS origin', + ) + } + expect(() => loadApiConfig({ ...env, XCS_HOSTED_PAYLOADS_ENABLED: 'yes' })).toThrow( + 'exactly true or false', + ) + expect(() => loadApiConfig({ ...env, XCS_PAYLOAD_STORAGE_IP_HASH_SECRET: undefined })).toThrow( + 'XCS_PAYLOAD_STORAGE_IP_HASH_SECRET is required', + ) + }) it('uses repository-standard XCS variables and disables fetching by default', () => { const config = loadApiConfig({ XCS_DATABASE_URL: 'postgres://xcs:xcs@localhost/xcs', diff --git a/apps/api/test/hosted-payloads.test.ts b/apps/api/test/hosted-payloads.test.ts new file mode 100644 index 0000000..8749e2e --- /dev/null +++ b/apps/api/test/hosted-payloads.test.ts @@ -0,0 +1,291 @@ +import { computeSchemaUid, encodeCredentialPayload, payloadDigest } from '@xcs-protocol/core' +import { buildCredentialCreate } from '@xcs-protocol/sdk' +import { describe, expect, it, vi } from 'vitest' +import { Wallet } from 'xrpl' + +import { + HostedPayloadService, + hostedPayloadLocator, + type HostedPayloadRecord, + type HostedPayloadRepository, +} from '../src/hosted-payloads.js' +import { HostedPayloadResolver } from '../src/hosted-payload-resolver.js' +import type { ApiRepository } from '../src/types.js' +import { createApi } from '../src/app.js' +import { DisabledPayloadResolver } from '../src/payload-resolver.js' +import { StaticTrustPolicy } from '../src/verification.js' + +// Test repositories model projection/storage boundaries; signatures and canonical payload validation are real. +function fixture(claims: Record = { course: 'Test course' }) { + const issuer = Wallet.generate() + const subject = Wallet.generate().address + const now = new Date() + const fields = Object.fromEntries( + Object.keys(claims).map((key) => [key, { type: 'string' as const }]), + ) + const definition = { + xcsVersion: '0.1' as const, + name: 'Completion', + description: 'Test only', + fields, + } + const ledgerHash = 'a'.repeat(64) + const uid = computeSchemaUid({ + schema: definition, + networkId: 1, + ledgerHash, + ledgerIndex: 1, + transactionIndex: 0, + publisher: issuer.address, + }) + const schema = { + profileId: 'testnet', + schemaUid: uid, + publisher: issuer.address, + name: definition.name, + description: definition.description, + parentUid: null, + supersedesUid: null, + definition, + resolvedDefinition: { definition, fields, lineage: [] }, + ledgerIndex: 1, + transactionIndex: 0, + registrationTransactionHash: 'b'.repeat(64), + } + const evidence = { + schema, + registration: { + profileId: 'testnet', + transactionHash: schema.registrationTransactionHash, + ledgerIndex: 1, + ledgerHash, + transactionIndex: 0, + publisher: issuer.address, + status: 'accepted', + reasonCode: null, + schemaUid: uid, + memoJson: definition, + }, + } + const payload = encodeCredentialPayload(claims, { + issuer: issuer.address, + subject, + schemaUid: uid, + fields, + }) + const digest = payloadDigest(payload.bytes) + const locator = hostedPayloadLocator(digest) + const uri = `https://payload.test/p/${locator}#xcs-sha256=${digest}` + const transaction = buildCredentialCreate({ + issuer: issuer.address, + subject, + schemaUid: uid, + uri, + }) + const signed = issuer.sign({ ...transaction, Fee: '12', Sequence: 1, LastLedgerSequence: 20 }) + const event = { eventType: 'created', uriHex: transaction.URI, ledgerIndex: 2 } + const api = { + withConsistentSnapshot: async (fn: (repository: ApiRepository) => Promise) => + fn(api as unknown as ApiRepository), + getNetwork: async () => ({ + profileId: 'testnet', + xcsVersion: '0.1', + networkId: 1, + requiredAmendment: 'c'.repeat(64), + registryAddress: issuer.address, + registrationAmountDrops: 1, + activationLedgerIndex: 1, + activationLedgerHash: 'd'.repeat(64), + }), + getDatabaseTime: async () => now, + getIndexerStatus: async () => ({ + profileId: 'testnet', + state: 'ready', + primarySourceTip: 10, + secondarySourceTip: 10, + lastAgreedLedgerIndex: 10, + lastAgreedLedgerHash: ledgerHash, + errorCode: null, + writerId: 'test-writer', + writerEpoch: 1, + leaseExpiresAt: new Date(now.getTime() + 60000), + updatedAt: now, + }), + getLatestCheckpoint: async () => ({ + profileId: 'testnet', + ledgerIndex: 10, + ledgerHash, + closeTime: Math.floor(now.getTime() / 1000) - 946684800, + }), + getSchema: async () => schema, + getSchemaProjectionEvidence: async () => [evidence], + getCredentialEventsByTransaction: vi.fn(async () => [event]), + } + let stored: HostedPayloadRecord | undefined + const repository: HostedPayloadRepository = { + get: vi.fn(async () => stored), + publish: vi.fn(async (input) => { + stored = { locator: input.locator, digestHex: input.digestHex, content: input.content } + return stored + }), + } + const service = new HostedPayloadService({ + repository, + apiRepository: api as unknown as ApiRepository, + publicBaseUrl: 'https://payload.test', + ipHashSecret: 'test-only-ip-hash-secret-000000000000', + enabledNetworks: new Set(['testnet']), + }) + const input = { + network: 'testnet', + locator, + payloadBase64: Buffer.from(payload.bytes).toString('base64'), + signedTransactionBlob: signed.tx_blob, + ipAddress: '127.0.0.1', + } + return { service, repository, api, input, signed, payload, digest, uri } +} + +describe('hosted public payload authorization', () => { + it('serves the publication HTTP contract and immutable exact bytes', async () => { + const f = fixture() + const app = await createApi({ + repository: f.api as unknown as ApiRepository, + resolver: new DisabledPayloadResolver(), + trustPolicy: new StaticTrustPolicy(), + hostedPayloadService: f.service, + }) + try { + const { ipAddress: _ip, locator, ...body } = f.input + const publication = await app.inject({ + method: 'POST', + url: `/v1/payloads/${locator}`, + payload: body, + }) + expect(publication.statusCode).toBe(200) + expect(publication.json()).toMatchObject({ uri: f.uri }) + const payload = await app.inject({ method: 'GET', url: `/p/${locator}` }) + expect(payload.statusCode).toBe(200) + expect(payload.body).toBe(f.payload.json) + expect(payload.headers['cache-control']).toBe('public, max-age=31536000, immutable') + expect(payload.headers.etag).toBe(`"${f.digest}"`) + const oversized = await app.inject({ + method: 'POST', + url: `/v1/payloads/${locator}`, + payload: { ...body, payloadBase64: Buffer.alloc(65537).toString('base64') }, + }) + expect(oversized.statusCode).toBe(413) + expect(oversized.json()).toMatchObject({ error: 'PAYLOAD_SIZE_INVALID' }) + } finally { + await app.close() + } + }) + it('publishes exact canonical bytes with a valid indexed CredentialCreate signature', async () => { + const f = fixture() + expect(await f.service.publish(f.input)).toMatchObject({ + uri: f.uri, + transactionHash: f.signed.hash.toLowerCase(), + byteLength: f.payload.bytes.length, + }) + expect(await f.service.get(f.input.locator)).toMatchObject({ + content: f.payload.json, + digestHex: f.digest, + }) + expect(f.api.getCredentialEventsByTransaction).toHaveBeenCalledWith( + expect.objectContaining({ transactionHash: f.signed.hash.toLowerCase() }), + ) + }) + + it.each(['!!!=', '', 'e30'])( + 'rejects malformed base64 before storage: %s', + async (payloadBase64) => { + const f = fixture() + await expect(f.service.publish({ ...f.input, payloadBase64 })).rejects.toMatchObject({ + code: 'PAYLOAD_BASE64_INVALID', + }) + expect(f.repository.publish).not.toHaveBeenCalled() + }, + ) + + it('rejects decoded payloads larger than 64 KiB before signature or storage', async () => { + const f = fixture() + await expect( + f.service.publish({ ...f.input, payloadBase64: Buffer.alloc(65537).toString('base64') }), + ).rejects.toMatchObject({ code: 'PAYLOAD_SIZE_INVALID', statusCode: 413 }) + expect(f.repository.publish).not.toHaveBeenCalled() + }) + + it('rejects unsigned data and altered signed transaction bytes', async () => { + const f = fixture() + for (const blob of [ + '00', + f.signed.tx_blob.slice(0, -2) + (f.signed.tx_blob.endsWith('FF') ? '00' : 'FF'), + ]) { + await expect( + f.service.publish({ ...f.input, signedTransactionBlob: blob }), + ).rejects.toMatchObject({ code: 'SIGNED_TRANSACTION_INVALID' }) + } + expect(f.repository.publish).not.toHaveBeenCalled() + }) + + it('requires exactly one matching indexed creation event before publication', async () => { + const f = fixture() + f.api.getCredentialEventsByTransaction.mockResolvedValue([]) + await expect(f.service.publish(f.input)).rejects.toMatchObject({ + code: 'SIGNED_TRANSACTION_NOT_INDEXED', + statusCode: 409, + }) + f.api.getCredentialEventsByTransaction.mockResolvedValue([ + { eventType: 'created', uriHex: '00', ledgerIndex: 2 }, + ]) + await expect(f.service.publish(f.input)).rejects.toMatchObject({ + code: 'SIGNED_TRANSACTION_NOT_INDEXED', + }) + expect(f.repository.publish).not.toHaveBeenCalled() + }) + + it("rejects another valid signer's transaction and duplicate projection events", async () => { + const f = fixture() + const other = fixture() + await expect( + f.service.publish({ ...f.input, signedTransactionBlob: other.input.signedTransactionBlob }), + ).rejects.toMatchObject({ code: 'SIGNED_TRANSACTION_PAYLOAD_MISMATCH' }) + const events = await f.api.getCredentialEventsByTransaction() + f.api.getCredentialEventsByTransaction.mockResolvedValue([...events, ...events]) + await expect(f.service.publish(f.input)).rejects.toMatchObject({ + code: 'SIGNED_TRANSACTION_NOT_INDEXED', + }) + expect(f.repository.publish).not.toHaveBeenCalled() + }) + + it('publishes schema-valid synthetic claims regardless of field names', async () => { + const f = fixture({ + prenom: 'Fictional', + firstname: 'Synthetic', + email: 'test@example.invalid', + }) + await f.service.publish(f.input) + expect(await f.service.get(f.input.locator)).toMatchObject({ + content: f.payload.json, + }) + }) + + it('keeps publication restricted to allowed Testnet profiles', async () => { + const f = fixture({ prenom: 'fictional' }) + await expect(f.service.publish({ ...f.input, network: 'mainnet' })).rejects.toMatchObject({ + code: 'PAYLOAD_HOSTING_NETWORK_DISABLED', + }) + expect(f.repository.publish).not.toHaveBeenCalled() + }) + + it('resolves its own immutable hosted URLs directly and delegates other origins', async () => { + const f = fixture() + await f.service.publish(f.input) + const external = { resolve: vi.fn(async () => new Uint8Array([1])) } + const resolver = new HostedPayloadResolver('https://payload.test', f.service, external) + expect(await resolver.resolve(f.uri)).toEqual(f.payload.bytes) + expect(external.resolve).not.toHaveBeenCalled() + await resolver.resolve(f.uri.replace('payload.test', 'other.test')) + expect(external.resolve).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/api/test/postgres.integration.test.ts b/apps/api/test/postgres.integration.test.ts index 80be3f5..ef2e617 100644 --- a/apps/api/test/postgres.integration.test.ts +++ b/apps/api/test/postgres.integration.test.ts @@ -7,6 +7,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { PostgresOperationalMetricsRepository } from '../src/operational-metrics-repository.js' import { PostgresPinningRepository } from '../src/pinning-repository.js' +import { PostgresHostedPayloadRepository } from '../src/hosted-payloads-repository.js' import { PostgresApiRepository } from '../src/repository.js' import { authoritativeSchemaCatalogBundle, @@ -210,6 +211,63 @@ describePostgres('PostgreSQL 18 API integration', () => { } }) + it('preserves hosted payload compatibility, idempotency, quotas and append-only runtime grants', async () => { + if (databaseClient === undefined || runtimeApiClient === undefined) + throw new Error('PostgreSQL test database is not initialized') + const profile = 'hosted-testnet' + const uid = '9'.repeat(64) + const txHash = '8'.repeat(64) + await databaseClient.sql`INSERT INTO network_profiles (profile_id,xcs_version,network_id,required_amendment,registry_address,registration_amount_drops,activation_ledger_index,activation_ledger_hash,enabled) VALUES (${profile},'0.1',1,${HASH},${PUBLISHER},1,1,${HASH},true)` + await databaseClient.sql`INSERT INTO schema_events (profile_id,transaction_hash,ledger_index,ledger_hash,transaction_index,publisher,status,schema_uid,memo_json) VALUES (${profile},${txHash},1,${HASH},0,${PUBLISHER},'accepted',${uid},'{}')` + await databaseClient.sql`INSERT INTO schemas (profile_id,schema_uid,publisher,name,description,definition,resolved_definition,registration_transaction_hash,ledger_index,transaction_index) VALUES (${profile},${uid},${PUBLISHER},'Hosted','Synthetic','{}','{}',${txHash},1,0)` + const repository = new PostgresHostedPayloadRepository(runtimeApiClient.db) + const input = { + locator: '1'.repeat(18), + digestHex: '1'.repeat(64), + content: '{}', + transactionHash: '2'.repeat(64), + profileId: profile, + issuer: PUBLISHER, + subject: PUBLISHER, + schemaUid: uid, + requesterIpHash: '3'.repeat(64), + now: new Date(), + dailyLimit: 2, + } + const stored = await repository.publish(input) + expect(await repository.publish(input)).toEqual(stored) + const legacy = { ...input, locator: '1'.repeat(20), transactionHash: '4'.repeat(64) } + expect(await repository.publish(legacy)).toMatchObject({ locator: legacy.locator }) + await expect( + repository.publish({ ...input, transactionHash: '5'.repeat(64) }), + ).rejects.toMatchObject({ code: 'PAYLOAD_PUBLICATION_QUOTA_EXCEEDED' }) + await expect( + repository.publish({ ...input, subject: 'rLs1MzkFWCxTbuAHgjeTZK4fcCDDnf2KRv' }), + ).rejects.toMatchObject({ code: 'PAYLOAD_PUBLICATION_CONFLICT' }) + await expect( + repository.publish({ + ...input, + transactionHash: '6'.repeat(64), + content: '{"changed":true}', + dailyLimit: 50, + }), + ).rejects.toMatchObject({ code: 'PAYLOAD_LOCATOR_COLLISION' }) + await expect( + runtimeApiClient.sql`UPDATE hosted_payloads SET content='changed' WHERE locator=${input.locator}`, + ).rejects.toMatchObject({ code: '42501' }) + await expect( + runtimeApiClient.sql`DELETE FROM hosted_payloads WHERE locator=${input.locator}`, + ).rejects.toMatchObject({ code: '42501' }) + await expect( + databaseClient.sql`INSERT INTO hosted_payloads (locator,digest_hex,content) VALUES (${'a'.repeat(18)},${'a'.repeat(64)},${'x'.repeat(65537)})`, + ).rejects.toMatchObject({ code: '23514' }) + await databaseClient.sql`DELETE FROM hosted_payload_publications WHERE profile_id=${profile}` + await databaseClient.sql`DELETE FROM hosted_payloads WHERE locator IN (${input.locator}, ${legacy.locator})` + await databaseClient.sql`DELETE FROM schemas WHERE profile_id=${profile}` + await databaseClient.sql`DELETE FROM schema_events WHERE profile_id=${profile}` + await databaseClient.sql`DELETE FROM network_profiles WHERE profile_id=${profile}` + }) + it('decodes database time inside an authoritative read snapshot', async () => { if (runtimeApiClient === undefined) { throw new Error('PostgreSQL test database is not initialized') diff --git a/docs/runbooks/local-source-recovery.md b/docs/runbooks/local-source-recovery.md new file mode 100644 index 0000000..dcc56b7 --- /dev/null +++ b/docs/runbooks/local-source-recovery.md @@ -0,0 +1,109 @@ +# Local source recovery — 2026-09-15 + +The running local HTTPS site was built from `/private/tmp/xcs-pr22-ready`, whose source files had +expired. The top-level checkout is an older implementation and must not replace that deployment. + +The durable development checkout is now `/Users/sanka/dev/xcs-protocol/.data/xcs-live`. +It was cloned locally from `fix/pr22-ready` (`3fced395`), without modifying the original checkout. +Newer web application files, configuration, assets and documentation were recovered from the +running `xcs-local-site-web-1` container. The deployed core bundle differs only by the public +`payloadDigest` export, restored in source. The deployed SDK source map supplied the exact newer +`submission.ts`. No wallet profiles, environment secrets or database contents were copied. + +Recovered changes predate the current wallet fix. Git diff against the PR therefore includes both +recovered work and the new fixes; do not treat the whole diff as newly authored work. + +## Payload API recovery — 2026-09-21 + +The deployed API's readable JavaScript modules and declarations were used to restore the missing +hosted-payload service, repository, resolver, HTTP routes, configuration and startup wiring. The API +declares its existing SDK dependency explicitly. The recovered database schema, runtime grants and +original migrations `0001_hosted_payloads` / `0002_hosted_payload_locator_compatibility` are restored +alongside the source. Their SQL bytes match the deployed migrations; do not reformat or rewrite them. +The original `0000` baseline is unchanged. Existing deployed databases already have these migrations. + +The restored service keeps the deployed authorization boundary: Testnet allowlist, canonical payload +and schema checks, a cryptographically valid matching `CredentialCreate`, fresh indexed creation +evidence, 64 KiB limit, quotas and immutable content. Legacy 20-character +locators remain readable; new publications use 18 characters. No private keys are handled by the API. + +Hosted Testnet publication no longer rejects claims based on field names such as `prenom`, +`firstname` or `email`. Claims must still match the registered schema. Use synthetic data only: +payloads are public, and accepting a field name does not establish that its value is non-personal. +The separate demo-pinning and browser-local test-storage policies are unchanged. + +Rebuild from this durable checkout, not the older parent checkout: + +```sh +pnpm install --frozen-lockfile +pnpm --filter @xcs-protocol/api... build +docker build -f docker/Dockerfile.node --build-arg XCS_PACKAGE=@xcs-protocol/api -t xcs-api:hosted-source-recovery . +docker build -f docker/Dockerfile.node --build-arg XCS_PACKAGE=@xcs-protocol/web -t xcs-web:publication-recovery . +``` + +For a fresh database or an older baseline-only deployment, run the existing secret-file bootstrap +workflow before starting the recovered API. It applies migrations and provisions append-only +`xcs_api` access to the hosted tables. Back up an existing database first; never reset the projection +or delete payloads as part of this rollout. Roll back application images without reversing these +compatible table additions. Use the same fixed HTTPS origin and existing secret files. + +API hosting configuration: `XCS_HOSTED_PAYLOADS_ENABLED=true`, +`XCS_PUBLIC_PAYLOAD_BASE_URL` (exact short HTTPS origin), `XCS_HOSTED_PAYLOAD_NETWORKS` +(allowed Testnet profile IDs) and `XCS_PAYLOAD_STORAGE_IP_HASH_SECRET` (at least 32 bytes). +The web's `NUXT_PUBLIC_PAYLOAD_BASE_URL` must match the API origin. Keep secrets out of source and +command output. GET `/p/:locator` serves immutable JSON; POST `/v1/payloads/:locator` publishes only +after validating its signed transaction and indexed evidence. + +The web now rejects oversized hosted payloads before signing and keeps consented recovery jobs +across reloads. See the web README for retry, retention and cleanup semantics. + +Verification on 2026-09-21: + +- Web: 244 unit tests and Nuxt typecheck passed; API: 231 unit tests; DB: 38 unit tests. +- Five API integration tests passed against an isolated PostgreSQL 18 instance, including bootstrap, + append-only permissions, hosted idempotency/quotas, legacy locators and size constraints. +- API/DB builds and typechecks passed. `pnpm --filter @xcs-protocol/db db:generate` reported no drift. +- Both Docker images built from source with the frozen lockfile. The local API and web were replaced + without replacing the database or indexer; the API still reported signing readiness `ready`. +- Brave, with normal certificate validation and the real Crossmark extension, showed Crossmark + available on the first chooser opening. This is discovery evidence, not a new credential-signing + compatibility claim; Crossmark 0.2.19 still rejects native Credential transactions. +- A recovery drill used an existing validated Testnet `CredentialCreate` blob fetched with + `binary:true`, and its real hosted payload. Reloading the recovery UI and retrying against the + rebuilt API returned 200. The unsigned-copy/signed-journal interruption case also recovered; + browser IndexedDB confirmed terminal signature cleanup. These were idempotent publication retries, + not new ledger transactions or simulated wallet approvals. The credential remained ACTIVE with + VALID schema and payload when checked without a connected wallet. + +## Historical recovery verification (2026-09-15) + +Original repair scope: + +- Wallet pending/connected/error states, bounded cancellation, and Crossmark session invalidation. +- Official adapter/Vue-binding regression coverage; removal of obsolete rc.0 patch tests. +- Indexer normalization of synthetic API metadata, retaining strict canonical ledger comparison. + +Use the existing local HTTPS Compose deployment and its private runtime env file. Do not copy +secrets into this checkout. Keep prior Docker images available for rollback; no ledger checkpoints +or PostgreSQL data should be reset for these repairs. + +## Verification and limitations + +- `pnpm --filter @xcs-protocol/web typecheck` passes. +- The web unit suite passes, including official Crossmark adapter + Vue binding integration + against extension-transport test doubles, not injected production accounts. +- Wallet UI and essential schema/issue/accept/verify Playwright cases pass in the explicitly + isolated browser test environment. These are not real extension approvals or live ledger writes. +- Production web and indexer Docker builds pass. Brave loads `/learn` with normal certificate + validation (HTTP 200, TLS 1.3), no page errors and no production E2E adapter. +- The indexer resumed past ledger `20711127`; its API reports `catching_up` with no source error. + Signing readiness remains blocked by the existing backlog. No checkpoint was skipped. +- The user's real Crossmark approval remains a manual retest. No wallet secrets were accessed. +- rc.2 declares an `xrpl` 3/4 peer range; this existing application uses `xrpl` 5. The tested paths + pass, but the warning remains and does not establish compatibility for every supported wallet. + +Rollback images remain `xcs-web:local-https` and +`sha256:f8fc6b5f4c758a6cbfa5f477d15ba6e7c63d45f94c057e214e3d13b261478ddd` (indexer). +Restore only the corresponding Compose image entry and recreate that service; leave databases, +profile files, wallet profiles and certificates untouched. The prior indexer image retains the +known synthetic-metadata divergence bug. diff --git a/packages/db/drizzle/0001_hosted_payloads.sql b/packages/db/drizzle/0001_hosted_payloads.sql new file mode 100644 index 0000000..0d7dc76 --- /dev/null +++ b/packages/db/drizzle/0001_hosted_payloads.sql @@ -0,0 +1,33 @@ +CREATE TABLE "hosted_payload_publications" ( + "transaction_hash" text PRIMARY KEY NOT NULL, + "locator" text NOT NULL, + "profile_id" text NOT NULL, + "issuer" text NOT NULL, + "subject" text NOT NULL, + "schema_uid" text NOT NULL, + "requester_ip_hash" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "hosted_payload_publications_tx_hash" CHECK ("hosted_payload_publications"."transaction_hash" ~ '^[0-9a-f]{64}$'), + CONSTRAINT "hosted_payload_publications_issuer" CHECK ("hosted_payload_publications"."issuer" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'), + CONSTRAINT "hosted_payload_publications_subject" CHECK ("hosted_payload_publications"."subject" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'), + CONSTRAINT "hosted_payload_publications_schema" CHECK ("hosted_payload_publications"."schema_uid" ~ '^[0-9a-f]{64}$'), + CONSTRAINT "hosted_payload_publications_ip_hash" CHECK ("hosted_payload_publications"."requester_ip_hash" ~ '^[0-9a-f]{64}$') +); +--> statement-breakpoint +CREATE TABLE "hosted_payloads" ( + "locator" text PRIMARY KEY NOT NULL, + "digest_hex" text NOT NULL, + "content" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "hosted_payloads_digest_hex_unique" UNIQUE("digest_hex"), + CONSTRAINT "hosted_payloads_locator" CHECK ("hosted_payloads"."locator" ~ '^[0-9a-f]{20}$'), + CONSTRAINT "hosted_payloads_digest" CHECK ("hosted_payloads"."digest_hex" ~ '^[0-9a-f]{64}$'), + CONSTRAINT "hosted_payloads_size" CHECK (octet_length("hosted_payloads"."content") BETWEEN 1 AND 65536) +); +--> statement-breakpoint +ALTER TABLE "hosted_payload_publications" ADD CONSTRAINT "hosted_payload_publications_locator_hosted_payloads_locator_fk" FOREIGN KEY ("locator") REFERENCES "public"."hosted_payloads"("locator") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hosted_payload_publications" ADD CONSTRAINT "hosted_payload_publications_profile_id_network_profiles_profile_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."network_profiles"("profile_id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hosted_payload_publications" ADD CONSTRAINT "hosted_payload_publications_schema_fk" FOREIGN KEY ("profile_id","schema_uid") REFERENCES "public"."schemas"("profile_id","schema_uid") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "hosted_payload_publications_wallet_quota_idx" ON "hosted_payload_publications" USING btree ("issuer","created_at");--> statement-breakpoint +CREATE INDEX "hosted_payload_publications_ip_quota_idx" ON "hosted_payload_publications" USING btree ("requester_ip_hash","created_at");--> statement-breakpoint +CREATE INDEX "hosted_payload_publications_locator_idx" ON "hosted_payload_publications" USING btree ("locator"); \ No newline at end of file diff --git a/packages/db/drizzle/0002_hosted_payload_locator_compatibility.sql b/packages/db/drizzle/0002_hosted_payload_locator_compatibility.sql new file mode 100644 index 0000000..84a22a9 --- /dev/null +++ b/packages/db/drizzle/0002_hosted_payload_locator_compatibility.sql @@ -0,0 +1,3 @@ +ALTER TABLE "hosted_payloads" DROP CONSTRAINT "hosted_payloads_digest_hex_unique";--> statement-breakpoint +ALTER TABLE "hosted_payloads" DROP CONSTRAINT "hosted_payloads_locator";--> statement-breakpoint +ALTER TABLE "hosted_payloads" ADD CONSTRAINT "hosted_payloads_locator" CHECK ("hosted_payloads"."locator" ~ '^(?:[0-9a-f]{18}|[0-9a-f]{20})$'); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0001_snapshot.json b/packages/db/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..a4b1d92 --- /dev/null +++ b/packages/db/drizzle/meta/0001_snapshot.json @@ -0,0 +1,2110 @@ +{ + "id": "d03daadc-770c-4ac8-ab74-90274a6caefd", + "prevId": "bd51fdfe-5c40-4e4c-995d-4bf5c9877561", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ledger_checkpoints": { + "name": "ledger_checkpoints", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_index": { + "name": "ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ledger_hash": { + "name": "ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_hash": { + "name": "parent_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "close_time": { + "name": "close_time", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "transaction_count": { + "name": "transaction_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "transaction_root": { + "name": "transaction_root", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ledger_checkpoints_profile_hash_uq": { + "name": "ledger_checkpoints_profile_hash_uq", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_checkpoints_profile_id_network_profiles_profile_id_fk": { + "name": "ledger_checkpoints_profile_id_network_profiles_profile_id_fk", + "tableFrom": "ledger_checkpoints", + "tableTo": "network_profiles", + "columnsFrom": ["profile_id"], + "columnsTo": ["profile_id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ledger_checkpoints_pk": { + "name": "ledger_checkpoints_pk", + "columns": ["profile_id", "ledger_index"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ledger_checkpoints_index_uint32": { + "name": "ledger_checkpoints_index_uint32", + "value": "\"ledger_checkpoints\".\"ledger_index\" BETWEEN 0 AND 4294967295" + }, + "ledger_checkpoints_hash": { + "name": "ledger_checkpoints_hash", + "value": "\"ledger_checkpoints\".\"ledger_hash\" ~ '^[0-9a-f]{64}$'" + }, + "ledger_checkpoints_parent": { + "name": "ledger_checkpoints_parent", + "value": "\"ledger_checkpoints\".\"parent_hash\" ~ '^[0-9a-f]{64}$'" + }, + "ledger_checkpoints_close_time_uint32": { + "name": "ledger_checkpoints_close_time_uint32", + "value": "\"ledger_checkpoints\".\"close_time\" BETWEEN 0 AND 4294967295" + }, + "ledger_checkpoints_tx_count": { + "name": "ledger_checkpoints_tx_count", + "value": "\"ledger_checkpoints\".\"transaction_count\" >= 0" + }, + "ledger_checkpoints_transaction_root": { + "name": "ledger_checkpoints_transaction_root", + "value": "\"ledger_checkpoints\".\"transaction_root\" ~ '^[0-9a-f]{64}$'" + } + }, + "isRLSEnabled": false + }, + "public.network_profiles": { + "name": "network_profiles", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "xcs_version": { + "name": "xcs_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network_id": { + "name": "network_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "required_amendment": { + "name": "required_amendment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registry_address": { + "name": "registry_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registration_amount_drops": { + "name": "registration_amount_drops", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "activation_ledger_index": { + "name": "activation_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "activation_ledger_hash": { + "name": "activation_ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "network_profiles_xcs_version": { + "name": "network_profiles_xcs_version", + "value": "\"network_profiles\".\"xcs_version\" = '0.1'" + }, + "network_profiles_network_id": { + "name": "network_profiles_network_id", + "value": "\"network_profiles\".\"network_id\" BETWEEN 0 AND 4294967295" + }, + "network_profiles_registration_amount": { + "name": "network_profiles_registration_amount", + "value": "\"network_profiles\".\"registration_amount_drops\" = 1" + }, + "network_profiles_activation_index": { + "name": "network_profiles_activation_index", + "value": "\"network_profiles\".\"activation_ledger_index\" BETWEEN 1 AND 4294967295" + }, + "network_profiles_activation_hash": { + "name": "network_profiles_activation_hash", + "value": "\"network_profiles\".\"activation_ledger_hash\" ~ '^[0-9a-f]{64}$'" + }, + "network_profiles_registry_address": { + "name": "network_profiles_registry_address", + "value": "\"network_profiles\".\"registry_address\" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'" + } + }, + "isRLSEnabled": false + }, + "public.indexer_incidents": { + "name": "indexer_incidents", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "writer_epoch": { + "name": "writer_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_source_tip": { + "name": "primary_source_tip", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "secondary_source_tip": { + "name": "secondary_source_tip", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_agreed_ledger_index": { + "name": "last_agreed_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_agreed_ledger_hash": { + "name": "last_agreed_ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "indexer_incidents_profile_id_network_profiles_profile_id_fk": { + "name": "indexer_incidents_profile_id_network_profiles_profile_id_fk", + "tableFrom": "indexer_incidents", + "tableTo": "network_profiles", + "columnsFrom": ["profile_id"], + "columnsTo": ["profile_id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "indexer_incidents_pk": { + "name": "indexer_incidents_pk", + "columns": ["profile_id", "writer_epoch"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "indexer_incidents_writer_epoch": { + "name": "indexer_incidents_writer_epoch", + "value": "\"indexer_incidents\".\"writer_epoch\" BETWEEN 1 AND 9007199254740991" + }, + "indexer_incidents_error_code": { + "name": "indexer_incidents_error_code", + "value": "\"indexer_incidents\".\"error_code\" ~ '^[A-Z][A-Z0-9_]{0,63}$'" + }, + "indexer_incidents_primary_tip": { + "name": "indexer_incidents_primary_tip", + "value": "\"indexer_incidents\".\"primary_source_tip\" IS NULL OR \"indexer_incidents\".\"primary_source_tip\" BETWEEN 0 AND 4294967295" + }, + "indexer_incidents_secondary_tip": { + "name": "indexer_incidents_secondary_tip", + "value": "\"indexer_incidents\".\"secondary_source_tip\" IS NULL OR \"indexer_incidents\".\"secondary_source_tip\" BETWEEN 0 AND 4294967295" + }, + "indexer_incidents_agreed_ledger": { + "name": "indexer_incidents_agreed_ledger", + "value": "(\"indexer_incidents\".\"last_agreed_ledger_index\" IS NULL AND \"indexer_incidents\".\"last_agreed_ledger_hash\" IS NULL)\n OR (\"indexer_incidents\".\"last_agreed_ledger_index\" IS NOT NULL\n AND \"indexer_incidents\".\"last_agreed_ledger_hash\" IS NOT NULL\n AND \"indexer_incidents\".\"last_agreed_ledger_index\" BETWEEN 0 AND 4294967295\n AND \"indexer_incidents\".\"last_agreed_ledger_hash\" ~ '^[0-9a-f]{64}$')" + } + }, + "isRLSEnabled": false + }, + "public.indexer_status": { + "name": "indexer_status", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_source_tip": { + "name": "primary_source_tip", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "secondary_source_tip": { + "name": "secondary_source_tip", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_agreed_ledger_index": { + "name": "last_agreed_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_agreed_ledger_hash": { + "name": "last_agreed_ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "writer_id": { + "name": "writer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "writer_epoch": { + "name": "writer_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "indexer_status_profile_id_network_profiles_profile_id_fk": { + "name": "indexer_status_profile_id_network_profiles_profile_id_fk", + "tableFrom": "indexer_status", + "tableTo": "network_profiles", + "columnsFrom": ["profile_id"], + "columnsTo": ["profile_id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "indexer_status_state": { + "name": "indexer_status_state", + "value": "\"indexer_status\".\"state\" IN ('starting', 'catching_up', 'ready', 'halted')" + }, + "indexer_status_primary_tip": { + "name": "indexer_status_primary_tip", + "value": "\"indexer_status\".\"primary_source_tip\" IS NULL OR \"indexer_status\".\"primary_source_tip\" BETWEEN 0 AND 4294967295" + }, + "indexer_status_secondary_tip": { + "name": "indexer_status_secondary_tip", + "value": "\"indexer_status\".\"secondary_source_tip\" IS NULL OR \"indexer_status\".\"secondary_source_tip\" BETWEEN 0 AND 4294967295" + }, + "indexer_status_agreed_ledger": { + "name": "indexer_status_agreed_ledger", + "value": "(\"indexer_status\".\"last_agreed_ledger_index\" IS NULL AND \"indexer_status\".\"last_agreed_ledger_hash\" IS NULL)\n OR (\"indexer_status\".\"last_agreed_ledger_index\" IS NOT NULL\n AND \"indexer_status\".\"last_agreed_ledger_hash\" IS NOT NULL\n AND \"indexer_status\".\"last_agreed_ledger_index\" BETWEEN 0 AND 4294967295\n AND \"indexer_status\".\"last_agreed_ledger_hash\" ~ '^[0-9a-f]{64}$')" + }, + "indexer_status_agreed_not_ahead": { + "name": "indexer_status_agreed_not_ahead", + "value": "\"indexer_status\".\"state\" = 'halted'\n OR \"indexer_status\".\"last_agreed_ledger_index\" IS NULL\n OR ((\"indexer_status\".\"primary_source_tip\" IS NULL OR \"indexer_status\".\"last_agreed_ledger_index\" <= \"indexer_status\".\"primary_source_tip\")\n AND (\"indexer_status\".\"secondary_source_tip\" IS NULL OR \"indexer_status\".\"last_agreed_ledger_index\" <= \"indexer_status\".\"secondary_source_tip\"))" + }, + "indexer_status_ready_shape": { + "name": "indexer_status_ready_shape", + "value": "\"indexer_status\".\"state\" <> 'ready'\n OR (\"indexer_status\".\"primary_source_tip\" IS NOT NULL\n AND \"indexer_status\".\"secondary_source_tip\" IS NOT NULL\n AND \"indexer_status\".\"last_agreed_ledger_index\" IS NOT NULL\n AND \"indexer_status\".\"last_agreed_ledger_hash\" IS NOT NULL\n AND \"indexer_status\".\"writer_id\" IS NOT NULL\n AND \"indexer_status\".\"lease_expires_at\" IS NOT NULL\n AND \"indexer_status\".\"last_agreed_ledger_index\" = LEAST(\"indexer_status\".\"primary_source_tip\", \"indexer_status\".\"secondary_source_tip\"))" + }, + "indexer_status_error_code": { + "name": "indexer_status_error_code", + "value": "\"indexer_status\".\"error_code\" IS NULL OR \"indexer_status\".\"error_code\" ~ '^[A-Z][A-Z0-9_]{0,63}$'" + }, + "indexer_status_error_shape": { + "name": "indexer_status_error_shape", + "value": "(\"indexer_status\".\"state\" = 'halted' AND \"indexer_status\".\"error_code\" IS NOT NULL)\n OR (\"indexer_status\".\"state\" <> 'halted' AND \"indexer_status\".\"error_code\" IS NULL)" + }, + "indexer_status_writer_id": { + "name": "indexer_status_writer_id", + "value": "\"indexer_status\".\"writer_id\" IS NULL OR \"indexer_status\".\"writer_id\" ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'" + }, + "indexer_status_writer_epoch": { + "name": "indexer_status_writer_epoch", + "value": "\"indexer_status\".\"writer_epoch\" BETWEEN 1 AND 9007199254740991" + }, + "indexer_status_lease_window": { + "name": "indexer_status_lease_window", + "value": "(\"indexer_status\".\"writer_id\" IS NULL AND \"indexer_status\".\"lease_expires_at\" IS NULL)\n OR (\"indexer_status\".\"writer_id\" IS NOT NULL\n AND \"indexer_status\".\"lease_expires_at\" IS NOT NULL\n AND \"indexer_status\".\"lease_expires_at\" >= \"indexer_status\".\"updated_at\"\n AND \"indexer_status\".\"lease_expires_at\" <= \"indexer_status\".\"updated_at\" + interval '5 minutes')" + } + }, + "isRLSEnabled": false + }, + "public.schema_events": { + "name": "schema_events", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_hash": { + "name": "transaction_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_index": { + "name": "ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ledger_hash": { + "name": "ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_index": { + "name": "transaction_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_uid": { + "name": "schema_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memo_json": { + "name": "memo_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schema_events_ledger_idx": { + "name": "schema_events_ledger_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schema_events_activity_idx": { + "name": "schema_events_activity_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schema_events_publisher_idx": { + "name": "schema_events_publisher_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schema_events_profile_id_network_profiles_profile_id_fk": { + "name": "schema_events_profile_id_network_profiles_profile_id_fk", + "tableFrom": "schema_events", + "tableTo": "network_profiles", + "columnsFrom": ["profile_id"], + "columnsTo": ["profile_id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "schema_events_pk": { + "name": "schema_events_pk", + "columns": ["profile_id", "transaction_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "schema_events_tx_hash": { + "name": "schema_events_tx_hash", + "value": "\"schema_events\".\"transaction_hash\" ~ '^[0-9a-f]{64}$'" + }, + "schema_events_ledger_hash": { + "name": "schema_events_ledger_hash", + "value": "\"schema_events\".\"ledger_hash\" ~ '^[0-9a-f]{64}$'" + }, + "schema_events_ledger_index_uint32": { + "name": "schema_events_ledger_index_uint32", + "value": "\"schema_events\".\"ledger_index\" BETWEEN 0 AND 4294967295" + }, + "schema_events_tx_index": { + "name": "schema_events_tx_index", + "value": "\"schema_events\".\"transaction_index\" >= 0" + }, + "schema_events_status": { + "name": "schema_events_status", + "value": "\"schema_events\".\"status\" IN ('accepted', 'rejected')" + }, + "schema_events_result_shape": { + "name": "schema_events_result_shape", + "value": "(\"schema_events\".\"status\" = 'accepted' AND \"schema_events\".\"schema_uid\" IS NOT NULL AND \"schema_events\".\"reason_code\" IS NULL AND \"schema_events\".\"memo_json\" IS NOT NULL)\n OR (\"schema_events\".\"status\" = 'rejected' AND \"schema_events\".\"schema_uid\" IS NULL AND \"schema_events\".\"reason_code\" IS NOT NULL)" + }, + "schema_events_schema_uid": { + "name": "schema_events_schema_uid", + "value": "\"schema_events\".\"schema_uid\" IS NULL OR \"schema_events\".\"schema_uid\" ~ '^[0-9a-f]{64}$'" + } + }, + "isRLSEnabled": false + }, + "public.schemas": { + "name": "schemas", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_uid": { + "name": "schema_uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_uid": { + "name": "parent_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "supersedes_uid": { + "name": "supersedes_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "resolved_definition": { + "name": "resolved_definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "registration_transaction_hash": { + "name": "registration_transaction_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_index": { + "name": "ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "transaction_index": { + "name": "transaction_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schemas_registration_tx_uq": { + "name": "schemas_registration_tx_uq", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_transaction_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schemas_publisher_order_idx": { + "name": "schemas_publisher_order_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schemas_order_idx": { + "name": "schemas_order_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schemas_search_idx": { + "name": "schemas_search_idx", + "columns": [ + { + "expression": "to_tsvector('simple', \"name\" || ' ' || \"description\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "schemas_profile_id_network_profiles_profile_id_fk": { + "name": "schemas_profile_id_network_profiles_profile_id_fk", + "tableFrom": "schemas", + "tableTo": "network_profiles", + "columnsFrom": ["profile_id"], + "columnsTo": ["profile_id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "schemas_registration_event_fk": { + "name": "schemas_registration_event_fk", + "tableFrom": "schemas", + "tableTo": "schema_events", + "columnsFrom": ["profile_id", "registration_transaction_hash"], + "columnsTo": ["profile_id", "transaction_hash"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "schemas_pk": { + "name": "schemas_pk", + "columns": ["profile_id", "schema_uid"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "schemas_uid": { + "name": "schemas_uid", + "value": "\"schemas\".\"schema_uid\" ~ '^[0-9a-f]{64}$'" + }, + "schemas_parent_uid": { + "name": "schemas_parent_uid", + "value": "\"schemas\".\"parent_uid\" IS NULL OR \"schemas\".\"parent_uid\" ~ '^[0-9a-f]{64}$'" + }, + "schemas_supersedes_uid": { + "name": "schemas_supersedes_uid", + "value": "\"schemas\".\"supersedes_uid\" IS NULL OR \"schemas\".\"supersedes_uid\" ~ '^[0-9a-f]{64}$'" + }, + "schemas_registration_tx_hash": { + "name": "schemas_registration_tx_hash", + "value": "\"schemas\".\"registration_transaction_hash\" ~ '^[0-9a-f]{64}$'" + }, + "schemas_ledger_index_uint32": { + "name": "schemas_ledger_index_uint32", + "value": "\"schemas\".\"ledger_index\" BETWEEN 0 AND 4294967295" + }, + "schemas_transaction_index": { + "name": "schemas_transaction_index", + "value": "\"schemas\".\"transaction_index\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.credential_events": { + "name": "credential_events", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_hash": { + "name": "transaction_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_index": { + "name": "node_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_object_id": { + "name": "ledger_object_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_index": { + "name": "ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ledger_hash": { + "name": "ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_index": { + "name": "transaction_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_uid": { + "name": "schema_uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri_hex": { + "name": "uri_hex", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiration": { + "name": "expiration", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "deletion_cause": { + "name": "deletion_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_events_generation_idx": { + "name": "credential_events_generation_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "node_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_events_exact_idx": { + "name": "credential_events_exact_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_uid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_events_profile_id_network_profiles_profile_id_fk": { + "name": "credential_events_profile_id_network_profiles_profile_id_fk", + "tableFrom": "credential_events", + "tableTo": "network_profiles", + "columnsFrom": ["profile_id"], + "columnsTo": ["profile_id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "credential_events_schema_fk": { + "name": "credential_events_schema_fk", + "tableFrom": "credential_events", + "tableTo": "schemas", + "columnsFrom": ["profile_id", "schema_uid"], + "columnsTo": ["profile_id", "schema_uid"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "credential_events_generation_fk": { + "name": "credential_events_generation_fk", + "tableFrom": "credential_events", + "tableTo": "credential_generations", + "columnsFrom": ["profile_id", "generation_id"], + "columnsTo": ["profile_id", "generation_id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_events_pk": { + "name": "credential_events_pk", + "columns": ["profile_id", "transaction_hash", "node_index"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_events_tx_hash": { + "name": "credential_events_tx_hash", + "value": "\"credential_events\".\"transaction_hash\" ~ '^[0-9a-f]{64}$'" + }, + "credential_events_object": { + "name": "credential_events_object", + "value": "\"credential_events\".\"ledger_object_id\" ~ '^[0-9a-f]{64}$'" + }, + "credential_events_ledger_hash": { + "name": "credential_events_ledger_hash", + "value": "\"credential_events\".\"ledger_hash\" ~ '^[0-9a-f]{64}$'" + }, + "credential_events_schema": { + "name": "credential_events_schema", + "value": "\"credential_events\".\"schema_uid\" ~ '^[0-9a-f]{64}$'" + }, + "credential_events_node_index": { + "name": "credential_events_node_index", + "value": "\"credential_events\".\"node_index\" >= 0" + }, + "credential_events_ledger_index_uint32": { + "name": "credential_events_ledger_index_uint32", + "value": "\"credential_events\".\"ledger_index\" BETWEEN 0 AND 4294967295" + }, + "credential_events_transaction_index": { + "name": "credential_events_transaction_index", + "value": "\"credential_events\".\"transaction_index\" >= 0" + }, + "credential_events_expiration_uint32": { + "name": "credential_events_expiration_uint32", + "value": "\"credential_events\".\"expiration\" IS NULL OR \"credential_events\".\"expiration\" BETWEEN 0 AND 4294967295" + }, + "credential_events_type": { + "name": "credential_events_type", + "value": "\"credential_events\".\"event_type\" IN ('created', 'accepted', 'deleted')" + }, + "credential_events_delete_shape": { + "name": "credential_events_delete_shape", + "value": "(\"credential_events\".\"event_type\" = 'deleted' AND \"credential_events\".\"deletion_cause\" IS NOT NULL)\n OR (\"credential_events\".\"event_type\" <> 'deleted' AND \"credential_events\".\"deletion_cause\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_generations": { + "name": "credential_generations", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_object_id": { + "name": "ledger_object_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_uid": { + "name": "schema_uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri_hex": { + "name": "uri_hex", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiration": { + "name": "expiration", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_ledger_index": { + "name": "created_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_transaction_index": { + "name": "created_transaction_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "last_ledger_index": { + "name": "last_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "deleted_ledger_index": { + "name": "deleted_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "deletion_cause": { + "name": "deletion_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_generations_live_uq": { + "name": "credential_generations_live_uq", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential_generations\".\"deleted_ledger_index\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_generations_exact_idx": { + "name": "credential_generations_exact_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_uid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_generations_stats_idx": { + "name": "credential_generations_stats_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expiration", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_generations_profile_fk": { + "name": "credential_generations_profile_fk", + "tableFrom": "credential_generations", + "tableTo": "network_profiles", + "columnsFrom": ["profile_id"], + "columnsTo": ["profile_id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "credential_generations_schema_fk": { + "name": "credential_generations_schema_fk", + "tableFrom": "credential_generations", + "tableTo": "schemas", + "columnsFrom": ["profile_id", "schema_uid"], + "columnsTo": ["profile_id", "schema_uid"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_generations_pk": { + "name": "credential_generations_pk", + "columns": ["profile_id", "generation_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_generations_id": { + "name": "credential_generations_id", + "value": "\"credential_generations\".\"generation_id\" ~ '^[0-9a-f]{64}$'" + }, + "credential_generations_object": { + "name": "credential_generations_object", + "value": "\"credential_generations\".\"ledger_object_id\" ~ '^[0-9a-f]{64}$'" + }, + "credential_generations_schema": { + "name": "credential_generations_schema", + "value": "\"credential_generations\".\"schema_uid\" ~ '^[0-9a-f]{64}$'" + }, + "credential_generations_expiration_uint32": { + "name": "credential_generations_expiration_uint32", + "value": "\"credential_generations\".\"expiration\" IS NULL OR \"credential_generations\".\"expiration\" BETWEEN 0 AND 4294967295" + }, + "credential_generations_created_ledger_uint32": { + "name": "credential_generations_created_ledger_uint32", + "value": "\"credential_generations\".\"created_ledger_index\" BETWEEN 0 AND 4294967295" + }, + "credential_generations_created_transaction_index": { + "name": "credential_generations_created_transaction_index", + "value": "\"credential_generations\".\"created_transaction_index\" >= 0" + }, + "credential_generations_last_ledger_uint32": { + "name": "credential_generations_last_ledger_uint32", + "value": "\"credential_generations\".\"last_ledger_index\" BETWEEN 0 AND 4294967295" + }, + "credential_generations_deleted_ledger_uint32": { + "name": "credential_generations_deleted_ledger_uint32", + "value": "\"credential_generations\".\"deleted_ledger_index\" IS NULL OR \"credential_generations\".\"deleted_ledger_index\" BETWEEN 0 AND 4294967295" + }, + "credential_generations_ledger_order": { + "name": "credential_generations_ledger_order", + "value": "\"credential_generations\".\"last_ledger_index\" >= \"credential_generations\".\"created_ledger_index\"\n AND (\"credential_generations\".\"deleted_ledger_index\" IS NULL OR \"credential_generations\".\"deleted_ledger_index\" = \"credential_generations\".\"last_ledger_index\")" + }, + "credential_generations_deletion": { + "name": "credential_generations_deletion", + "value": "(\"credential_generations\".\"deleted_ledger_index\" IS NULL AND \"credential_generations\".\"deletion_cause\" IS NULL)\n OR (\"credential_generations\".\"deleted_ledger_index\" IS NOT NULL AND \"credential_generations\".\"deletion_cause\" IS NOT NULL)" + }, + "credential_generations_deletion_cause": { + "name": "credential_generations_deletion_cause", + "value": "\"credential_generations\".\"deletion_cause\" IS NULL OR \"credential_generations\".\"deletion_cause\" IN ('issuer_revoked', 'subject_rejected', 'subject_removed', 'expired_cleanup', 'account_deleted', 'self_deleted')" + } + }, + "isRLSEnabled": false + }, + "public.demo_pins": { + "name": "demo_pins", + "schema": "", + "columns": { + "pin_id": { + "name": "pin_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "challenge_id": { + "name": "challenge_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_ip_hash": { + "name": "requester_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_length": { + "name": "byte_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "unpinned_at": { + "name": "unpinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "demo_pins_wallet_quota_idx": { + "name": "demo_pins_wallet_quota_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "demo_pins_ip_quota_idx": { + "name": "demo_pins_ip_quota_idx", + "columns": [ + { + "expression": "requester_ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "demo_pins_expiry_idx": { + "name": "demo_pins_expiry_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "demo_pins_cid_active_idx": { + "name": "demo_pins_cid_active_idx", + "columns": [ + { + "expression": "cid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "demo_pins_challenge_id_pin_challenges_challenge_id_fk": { + "name": "demo_pins_challenge_id_pin_challenges_challenge_id_fk", + "tableFrom": "demo_pins", + "tableTo": "pin_challenges", + "columnsFrom": ["challenge_id"], + "columnsTo": ["challenge_id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "demo_pins_profile_id_network_profiles_profile_id_fk": { + "name": "demo_pins_profile_id_network_profiles_profile_id_fk", + "tableFrom": "demo_pins", + "tableTo": "network_profiles", + "columnsFrom": ["profile_id"], + "columnsTo": ["profile_id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "demo_pins_challenge_id_unique": { + "name": "demo_pins_challenge_id_unique", + "nullsNotDistinct": false, + "columns": ["challenge_id"] + } + }, + "policies": {}, + "checkConstraints": { + "demo_pins_id": { + "name": "demo_pins_id", + "value": "\"demo_pins\".\"pin_id\" ~ '^[0-9a-f]{64}$'" + }, + "demo_pins_ip_hash": { + "name": "demo_pins_ip_hash", + "value": "\"demo_pins\".\"requester_ip_hash\" ~ '^[0-9a-f]{64}$'" + }, + "demo_pins_wallet": { + "name": "demo_pins_wallet", + "value": "\"demo_pins\".\"wallet\" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'" + }, + "demo_pins_cid": { + "name": "demo_pins_cid", + "value": "\"demo_pins\".\"cid\" ~ '^b[a-z2-7]+$'" + }, + "demo_pins_byte_length": { + "name": "demo_pins_byte_length", + "value": "\"demo_pins\".\"byte_length\" BETWEEN 1 AND 65536" + }, + "demo_pins_status": { + "name": "demo_pins_status", + "value": "\"demo_pins\".\"status\" IN ('pending', 'pinned', 'failed', 'unpinned')" + }, + "demo_pins_failure_shape": { + "name": "demo_pins_failure_shape", + "value": "(\"demo_pins\".\"status\" = 'failed' AND \"demo_pins\".\"failure_code\" IS NOT NULL)\n OR (\"demo_pins\".\"status\" <> 'failed' AND \"demo_pins\".\"failure_code\" IS NULL)" + }, + "demo_pins_unpinned_shape": { + "name": "demo_pins_unpinned_shape", + "value": "(\"demo_pins\".\"status\" = 'unpinned' AND \"demo_pins\".\"unpinned_at\" IS NOT NULL)\n OR (\"demo_pins\".\"status\" <> 'unpinned' AND \"demo_pins\".\"unpinned_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.pin_challenges": { + "name": "pin_challenges", + "schema": "", + "columns": { + "challenge_id": { + "name": "challenge_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_ip_hash": { + "name": "requester_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pin_challenges_wallet_created_idx": { + "name": "pin_challenges_wallet_created_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pin_challenges_ip_created_idx": { + "name": "pin_challenges_ip_created_idx", + "columns": [ + { + "expression": "requester_ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pin_challenges_expiry_idx": { + "name": "pin_challenges_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pin_challenges_profile_id_network_profiles_profile_id_fk": { + "name": "pin_challenges_profile_id_network_profiles_profile_id_fk", + "tableFrom": "pin_challenges", + "tableTo": "network_profiles", + "columnsFrom": ["profile_id"], + "columnsTo": ["profile_id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pin_challenges_id": { + "name": "pin_challenges_id", + "value": "\"pin_challenges\".\"challenge_id\" ~ '^[0-9a-f]{64}$'" + }, + "pin_challenges_ip_hash": { + "name": "pin_challenges_ip_hash", + "value": "\"pin_challenges\".\"requester_ip_hash\" ~ '^[0-9a-f]{64}$'" + }, + "pin_challenges_wallet": { + "name": "pin_challenges_wallet", + "value": "\"pin_challenges\".\"wallet\" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'" + }, + "pin_challenges_expiry": { + "name": "pin_challenges_expiry", + "value": "\"pin_challenges\".\"expires_at\" > \"pin_challenges\".\"created_at\"" + } + }, + "isRLSEnabled": false + }, + "public.hosted_payload_publications": { + "name": "hosted_payload_publications", + "schema": "", + "columns": { + "transaction_hash": { + "name": "transaction_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "locator": { + "name": "locator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_uid": { + "name": "schema_uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_ip_hash": { + "name": "requester_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hosted_payload_publications_wallet_quota_idx": { + "name": "hosted_payload_publications_wallet_quota_idx", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hosted_payload_publications_ip_quota_idx": { + "name": "hosted_payload_publications_ip_quota_idx", + "columns": [ + { + "expression": "requester_ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hosted_payload_publications_locator_idx": { + "name": "hosted_payload_publications_locator_idx", + "columns": [ + { + "expression": "locator", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hosted_payload_publications_locator_hosted_payloads_locator_fk": { + "name": "hosted_payload_publications_locator_hosted_payloads_locator_fk", + "tableFrom": "hosted_payload_publications", + "tableTo": "hosted_payloads", + "columnsFrom": ["locator"], + "columnsTo": ["locator"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "hosted_payload_publications_profile_id_network_profiles_profile_id_fk": { + "name": "hosted_payload_publications_profile_id_network_profiles_profile_id_fk", + "tableFrom": "hosted_payload_publications", + "tableTo": "network_profiles", + "columnsFrom": ["profile_id"], + "columnsTo": ["profile_id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "hosted_payload_publications_schema_fk": { + "name": "hosted_payload_publications_schema_fk", + "tableFrom": "hosted_payload_publications", + "tableTo": "schemas", + "columnsFrom": ["profile_id", "schema_uid"], + "columnsTo": ["profile_id", "schema_uid"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "hosted_payload_publications_tx_hash": { + "name": "hosted_payload_publications_tx_hash", + "value": "\"hosted_payload_publications\".\"transaction_hash\" ~ '^[0-9a-f]{64}$'" + }, + "hosted_payload_publications_issuer": { + "name": "hosted_payload_publications_issuer", + "value": "\"hosted_payload_publications\".\"issuer\" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'" + }, + "hosted_payload_publications_subject": { + "name": "hosted_payload_publications_subject", + "value": "\"hosted_payload_publications\".\"subject\" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'" + }, + "hosted_payload_publications_schema": { + "name": "hosted_payload_publications_schema", + "value": "\"hosted_payload_publications\".\"schema_uid\" ~ '^[0-9a-f]{64}$'" + }, + "hosted_payload_publications_ip_hash": { + "name": "hosted_payload_publications_ip_hash", + "value": "\"hosted_payload_publications\".\"requester_ip_hash\" ~ '^[0-9a-f]{64}$'" + } + }, + "isRLSEnabled": false + }, + "public.hosted_payloads": { + "name": "hosted_payloads", + "schema": "", + "columns": { + "locator": { + "name": "locator", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "digest_hex": { + "name": "digest_hex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hosted_payloads_digest_hex_unique": { + "name": "hosted_payloads_digest_hex_unique", + "nullsNotDistinct": false, + "columns": ["digest_hex"] + } + }, + "policies": {}, + "checkConstraints": { + "hosted_payloads_locator": { + "name": "hosted_payloads_locator", + "value": "\"hosted_payloads\".\"locator\" ~ '^[0-9a-f]{20}$'" + }, + "hosted_payloads_digest": { + "name": "hosted_payloads_digest", + "value": "\"hosted_payloads\".\"digest_hex\" ~ '^[0-9a-f]{64}$'" + }, + "hosted_payloads_size": { + "name": "hosted_payloads_size", + "value": "octet_length(\"hosted_payloads\".\"content\") BETWEEN 1 AND 65536" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/drizzle/meta/0002_snapshot.json b/packages/db/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..d8f8cb6 --- /dev/null +++ b/packages/db/drizzle/meta/0002_snapshot.json @@ -0,0 +1,2203 @@ +{ + "id": "8c531e24-4c32-4f12-8c21-aaaeae0f81b5", + "prevId": "d03daadc-770c-4ac8-ab74-90274a6caefd", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ledger_checkpoints": { + "name": "ledger_checkpoints", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_index": { + "name": "ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ledger_hash": { + "name": "ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_hash": { + "name": "parent_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "close_time": { + "name": "close_time", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "transaction_count": { + "name": "transaction_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "transaction_root": { + "name": "transaction_root", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ledger_checkpoints_profile_hash_uq": { + "name": "ledger_checkpoints_profile_hash_uq", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_checkpoints_profile_id_network_profiles_profile_id_fk": { + "name": "ledger_checkpoints_profile_id_network_profiles_profile_id_fk", + "tableFrom": "ledger_checkpoints", + "tableTo": "network_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "profile_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ledger_checkpoints_pk": { + "name": "ledger_checkpoints_pk", + "columns": [ + "profile_id", + "ledger_index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ledger_checkpoints_index_uint32": { + "name": "ledger_checkpoints_index_uint32", + "value": "\"ledger_checkpoints\".\"ledger_index\" BETWEEN 0 AND 4294967295" + }, + "ledger_checkpoints_hash": { + "name": "ledger_checkpoints_hash", + "value": "\"ledger_checkpoints\".\"ledger_hash\" ~ '^[0-9a-f]{64}$'" + }, + "ledger_checkpoints_parent": { + "name": "ledger_checkpoints_parent", + "value": "\"ledger_checkpoints\".\"parent_hash\" ~ '^[0-9a-f]{64}$'" + }, + "ledger_checkpoints_close_time_uint32": { + "name": "ledger_checkpoints_close_time_uint32", + "value": "\"ledger_checkpoints\".\"close_time\" BETWEEN 0 AND 4294967295" + }, + "ledger_checkpoints_tx_count": { + "name": "ledger_checkpoints_tx_count", + "value": "\"ledger_checkpoints\".\"transaction_count\" >= 0" + }, + "ledger_checkpoints_transaction_root": { + "name": "ledger_checkpoints_transaction_root", + "value": "\"ledger_checkpoints\".\"transaction_root\" ~ '^[0-9a-f]{64}$'" + } + }, + "isRLSEnabled": false + }, + "public.network_profiles": { + "name": "network_profiles", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "xcs_version": { + "name": "xcs_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network_id": { + "name": "network_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "required_amendment": { + "name": "required_amendment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registry_address": { + "name": "registry_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registration_amount_drops": { + "name": "registration_amount_drops", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "activation_ledger_index": { + "name": "activation_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "activation_ledger_hash": { + "name": "activation_ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "network_profiles_xcs_version": { + "name": "network_profiles_xcs_version", + "value": "\"network_profiles\".\"xcs_version\" = '0.1'" + }, + "network_profiles_network_id": { + "name": "network_profiles_network_id", + "value": "\"network_profiles\".\"network_id\" BETWEEN 0 AND 4294967295" + }, + "network_profiles_registration_amount": { + "name": "network_profiles_registration_amount", + "value": "\"network_profiles\".\"registration_amount_drops\" = 1" + }, + "network_profiles_activation_index": { + "name": "network_profiles_activation_index", + "value": "\"network_profiles\".\"activation_ledger_index\" BETWEEN 1 AND 4294967295" + }, + "network_profiles_activation_hash": { + "name": "network_profiles_activation_hash", + "value": "\"network_profiles\".\"activation_ledger_hash\" ~ '^[0-9a-f]{64}$'" + }, + "network_profiles_registry_address": { + "name": "network_profiles_registry_address", + "value": "\"network_profiles\".\"registry_address\" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'" + } + }, + "isRLSEnabled": false + }, + "public.indexer_incidents": { + "name": "indexer_incidents", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "writer_epoch": { + "name": "writer_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_source_tip": { + "name": "primary_source_tip", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "secondary_source_tip": { + "name": "secondary_source_tip", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_agreed_ledger_index": { + "name": "last_agreed_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_agreed_ledger_hash": { + "name": "last_agreed_ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "indexer_incidents_profile_id_network_profiles_profile_id_fk": { + "name": "indexer_incidents_profile_id_network_profiles_profile_id_fk", + "tableFrom": "indexer_incidents", + "tableTo": "network_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "profile_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "indexer_incidents_pk": { + "name": "indexer_incidents_pk", + "columns": [ + "profile_id", + "writer_epoch" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "indexer_incidents_writer_epoch": { + "name": "indexer_incidents_writer_epoch", + "value": "\"indexer_incidents\".\"writer_epoch\" BETWEEN 1 AND 9007199254740991" + }, + "indexer_incidents_error_code": { + "name": "indexer_incidents_error_code", + "value": "\"indexer_incidents\".\"error_code\" ~ '^[A-Z][A-Z0-9_]{0,63}$'" + }, + "indexer_incidents_primary_tip": { + "name": "indexer_incidents_primary_tip", + "value": "\"indexer_incidents\".\"primary_source_tip\" IS NULL OR \"indexer_incidents\".\"primary_source_tip\" BETWEEN 0 AND 4294967295" + }, + "indexer_incidents_secondary_tip": { + "name": "indexer_incidents_secondary_tip", + "value": "\"indexer_incidents\".\"secondary_source_tip\" IS NULL OR \"indexer_incidents\".\"secondary_source_tip\" BETWEEN 0 AND 4294967295" + }, + "indexer_incidents_agreed_ledger": { + "name": "indexer_incidents_agreed_ledger", + "value": "(\"indexer_incidents\".\"last_agreed_ledger_index\" IS NULL AND \"indexer_incidents\".\"last_agreed_ledger_hash\" IS NULL)\n OR (\"indexer_incidents\".\"last_agreed_ledger_index\" IS NOT NULL\n AND \"indexer_incidents\".\"last_agreed_ledger_hash\" IS NOT NULL\n AND \"indexer_incidents\".\"last_agreed_ledger_index\" BETWEEN 0 AND 4294967295\n AND \"indexer_incidents\".\"last_agreed_ledger_hash\" ~ '^[0-9a-f]{64}$')" + } + }, + "isRLSEnabled": false + }, + "public.indexer_status": { + "name": "indexer_status", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_source_tip": { + "name": "primary_source_tip", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "secondary_source_tip": { + "name": "secondary_source_tip", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_agreed_ledger_index": { + "name": "last_agreed_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_agreed_ledger_hash": { + "name": "last_agreed_ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "writer_id": { + "name": "writer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "writer_epoch": { + "name": "writer_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "indexer_status_profile_id_network_profiles_profile_id_fk": { + "name": "indexer_status_profile_id_network_profiles_profile_id_fk", + "tableFrom": "indexer_status", + "tableTo": "network_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "profile_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "indexer_status_state": { + "name": "indexer_status_state", + "value": "\"indexer_status\".\"state\" IN ('starting', 'catching_up', 'ready', 'halted')" + }, + "indexer_status_primary_tip": { + "name": "indexer_status_primary_tip", + "value": "\"indexer_status\".\"primary_source_tip\" IS NULL OR \"indexer_status\".\"primary_source_tip\" BETWEEN 0 AND 4294967295" + }, + "indexer_status_secondary_tip": { + "name": "indexer_status_secondary_tip", + "value": "\"indexer_status\".\"secondary_source_tip\" IS NULL OR \"indexer_status\".\"secondary_source_tip\" BETWEEN 0 AND 4294967295" + }, + "indexer_status_agreed_ledger": { + "name": "indexer_status_agreed_ledger", + "value": "(\"indexer_status\".\"last_agreed_ledger_index\" IS NULL AND \"indexer_status\".\"last_agreed_ledger_hash\" IS NULL)\n OR (\"indexer_status\".\"last_agreed_ledger_index\" IS NOT NULL\n AND \"indexer_status\".\"last_agreed_ledger_hash\" IS NOT NULL\n AND \"indexer_status\".\"last_agreed_ledger_index\" BETWEEN 0 AND 4294967295\n AND \"indexer_status\".\"last_agreed_ledger_hash\" ~ '^[0-9a-f]{64}$')" + }, + "indexer_status_agreed_not_ahead": { + "name": "indexer_status_agreed_not_ahead", + "value": "\"indexer_status\".\"state\" = 'halted'\n OR \"indexer_status\".\"last_agreed_ledger_index\" IS NULL\n OR ((\"indexer_status\".\"primary_source_tip\" IS NULL OR \"indexer_status\".\"last_agreed_ledger_index\" <= \"indexer_status\".\"primary_source_tip\")\n AND (\"indexer_status\".\"secondary_source_tip\" IS NULL OR \"indexer_status\".\"last_agreed_ledger_index\" <= \"indexer_status\".\"secondary_source_tip\"))" + }, + "indexer_status_ready_shape": { + "name": "indexer_status_ready_shape", + "value": "\"indexer_status\".\"state\" <> 'ready'\n OR (\"indexer_status\".\"primary_source_tip\" IS NOT NULL\n AND \"indexer_status\".\"secondary_source_tip\" IS NOT NULL\n AND \"indexer_status\".\"last_agreed_ledger_index\" IS NOT NULL\n AND \"indexer_status\".\"last_agreed_ledger_hash\" IS NOT NULL\n AND \"indexer_status\".\"writer_id\" IS NOT NULL\n AND \"indexer_status\".\"lease_expires_at\" IS NOT NULL\n AND \"indexer_status\".\"last_agreed_ledger_index\" = LEAST(\"indexer_status\".\"primary_source_tip\", \"indexer_status\".\"secondary_source_tip\"))" + }, + "indexer_status_error_code": { + "name": "indexer_status_error_code", + "value": "\"indexer_status\".\"error_code\" IS NULL OR \"indexer_status\".\"error_code\" ~ '^[A-Z][A-Z0-9_]{0,63}$'" + }, + "indexer_status_error_shape": { + "name": "indexer_status_error_shape", + "value": "(\"indexer_status\".\"state\" = 'halted' AND \"indexer_status\".\"error_code\" IS NOT NULL)\n OR (\"indexer_status\".\"state\" <> 'halted' AND \"indexer_status\".\"error_code\" IS NULL)" + }, + "indexer_status_writer_id": { + "name": "indexer_status_writer_id", + "value": "\"indexer_status\".\"writer_id\" IS NULL OR \"indexer_status\".\"writer_id\" ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'" + }, + "indexer_status_writer_epoch": { + "name": "indexer_status_writer_epoch", + "value": "\"indexer_status\".\"writer_epoch\" BETWEEN 1 AND 9007199254740991" + }, + "indexer_status_lease_window": { + "name": "indexer_status_lease_window", + "value": "(\"indexer_status\".\"writer_id\" IS NULL AND \"indexer_status\".\"lease_expires_at\" IS NULL)\n OR (\"indexer_status\".\"writer_id\" IS NOT NULL\n AND \"indexer_status\".\"lease_expires_at\" IS NOT NULL\n AND \"indexer_status\".\"lease_expires_at\" >= \"indexer_status\".\"updated_at\"\n AND \"indexer_status\".\"lease_expires_at\" <= \"indexer_status\".\"updated_at\" + interval '5 minutes')" + } + }, + "isRLSEnabled": false + }, + "public.schema_events": { + "name": "schema_events", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_hash": { + "name": "transaction_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_index": { + "name": "ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ledger_hash": { + "name": "ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_index": { + "name": "transaction_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_uid": { + "name": "schema_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memo_json": { + "name": "memo_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schema_events_ledger_idx": { + "name": "schema_events_ledger_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schema_events_activity_idx": { + "name": "schema_events_activity_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schema_events_publisher_idx": { + "name": "schema_events_publisher_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schema_events_profile_id_network_profiles_profile_id_fk": { + "name": "schema_events_profile_id_network_profiles_profile_id_fk", + "tableFrom": "schema_events", + "tableTo": "network_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "profile_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "schema_events_pk": { + "name": "schema_events_pk", + "columns": [ + "profile_id", + "transaction_hash" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "schema_events_tx_hash": { + "name": "schema_events_tx_hash", + "value": "\"schema_events\".\"transaction_hash\" ~ '^[0-9a-f]{64}$'" + }, + "schema_events_ledger_hash": { + "name": "schema_events_ledger_hash", + "value": "\"schema_events\".\"ledger_hash\" ~ '^[0-9a-f]{64}$'" + }, + "schema_events_ledger_index_uint32": { + "name": "schema_events_ledger_index_uint32", + "value": "\"schema_events\".\"ledger_index\" BETWEEN 0 AND 4294967295" + }, + "schema_events_tx_index": { + "name": "schema_events_tx_index", + "value": "\"schema_events\".\"transaction_index\" >= 0" + }, + "schema_events_status": { + "name": "schema_events_status", + "value": "\"schema_events\".\"status\" IN ('accepted', 'rejected')" + }, + "schema_events_result_shape": { + "name": "schema_events_result_shape", + "value": "(\"schema_events\".\"status\" = 'accepted' AND \"schema_events\".\"schema_uid\" IS NOT NULL AND \"schema_events\".\"reason_code\" IS NULL AND \"schema_events\".\"memo_json\" IS NOT NULL)\n OR (\"schema_events\".\"status\" = 'rejected' AND \"schema_events\".\"schema_uid\" IS NULL AND \"schema_events\".\"reason_code\" IS NOT NULL)" + }, + "schema_events_schema_uid": { + "name": "schema_events_schema_uid", + "value": "\"schema_events\".\"schema_uid\" IS NULL OR \"schema_events\".\"schema_uid\" ~ '^[0-9a-f]{64}$'" + } + }, + "isRLSEnabled": false + }, + "public.schemas": { + "name": "schemas", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_uid": { + "name": "schema_uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher": { + "name": "publisher", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_uid": { + "name": "parent_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "supersedes_uid": { + "name": "supersedes_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "resolved_definition": { + "name": "resolved_definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "registration_transaction_hash": { + "name": "registration_transaction_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_index": { + "name": "ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "transaction_index": { + "name": "transaction_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schemas_registration_tx_uq": { + "name": "schemas_registration_tx_uq", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_transaction_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schemas_publisher_order_idx": { + "name": "schemas_publisher_order_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schemas_order_idx": { + "name": "schemas_order_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "schemas_search_idx": { + "name": "schemas_search_idx", + "columns": [ + { + "expression": "to_tsvector('simple', \"name\" || ' ' || \"description\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "schemas_profile_id_network_profiles_profile_id_fk": { + "name": "schemas_profile_id_network_profiles_profile_id_fk", + "tableFrom": "schemas", + "tableTo": "network_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "profile_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "schemas_registration_event_fk": { + "name": "schemas_registration_event_fk", + "tableFrom": "schemas", + "tableTo": "schema_events", + "columnsFrom": [ + "profile_id", + "registration_transaction_hash" + ], + "columnsTo": [ + "profile_id", + "transaction_hash" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "schemas_pk": { + "name": "schemas_pk", + "columns": [ + "profile_id", + "schema_uid" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "schemas_uid": { + "name": "schemas_uid", + "value": "\"schemas\".\"schema_uid\" ~ '^[0-9a-f]{64}$'" + }, + "schemas_parent_uid": { + "name": "schemas_parent_uid", + "value": "\"schemas\".\"parent_uid\" IS NULL OR \"schemas\".\"parent_uid\" ~ '^[0-9a-f]{64}$'" + }, + "schemas_supersedes_uid": { + "name": "schemas_supersedes_uid", + "value": "\"schemas\".\"supersedes_uid\" IS NULL OR \"schemas\".\"supersedes_uid\" ~ '^[0-9a-f]{64}$'" + }, + "schemas_registration_tx_hash": { + "name": "schemas_registration_tx_hash", + "value": "\"schemas\".\"registration_transaction_hash\" ~ '^[0-9a-f]{64}$'" + }, + "schemas_ledger_index_uint32": { + "name": "schemas_ledger_index_uint32", + "value": "\"schemas\".\"ledger_index\" BETWEEN 0 AND 4294967295" + }, + "schemas_transaction_index": { + "name": "schemas_transaction_index", + "value": "\"schemas\".\"transaction_index\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.credential_events": { + "name": "credential_events", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_hash": { + "name": "transaction_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_index": { + "name": "node_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_object_id": { + "name": "ledger_object_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_index": { + "name": "ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ledger_hash": { + "name": "ledger_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_index": { + "name": "transaction_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_uid": { + "name": "schema_uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri_hex": { + "name": "uri_hex", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiration": { + "name": "expiration", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "deletion_cause": { + "name": "deletion_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_events_generation_idx": { + "name": "credential_events_generation_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "transaction_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "node_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_events_exact_idx": { + "name": "credential_events_exact_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_uid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_events_profile_id_network_profiles_profile_id_fk": { + "name": "credential_events_profile_id_network_profiles_profile_id_fk", + "tableFrom": "credential_events", + "tableTo": "network_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "profile_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "credential_events_schema_fk": { + "name": "credential_events_schema_fk", + "tableFrom": "credential_events", + "tableTo": "schemas", + "columnsFrom": [ + "profile_id", + "schema_uid" + ], + "columnsTo": [ + "profile_id", + "schema_uid" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "credential_events_generation_fk": { + "name": "credential_events_generation_fk", + "tableFrom": "credential_events", + "tableTo": "credential_generations", + "columnsFrom": [ + "profile_id", + "generation_id" + ], + "columnsTo": [ + "profile_id", + "generation_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_events_pk": { + "name": "credential_events_pk", + "columns": [ + "profile_id", + "transaction_hash", + "node_index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_events_tx_hash": { + "name": "credential_events_tx_hash", + "value": "\"credential_events\".\"transaction_hash\" ~ '^[0-9a-f]{64}$'" + }, + "credential_events_object": { + "name": "credential_events_object", + "value": "\"credential_events\".\"ledger_object_id\" ~ '^[0-9a-f]{64}$'" + }, + "credential_events_ledger_hash": { + "name": "credential_events_ledger_hash", + "value": "\"credential_events\".\"ledger_hash\" ~ '^[0-9a-f]{64}$'" + }, + "credential_events_schema": { + "name": "credential_events_schema", + "value": "\"credential_events\".\"schema_uid\" ~ '^[0-9a-f]{64}$'" + }, + "credential_events_node_index": { + "name": "credential_events_node_index", + "value": "\"credential_events\".\"node_index\" >= 0" + }, + "credential_events_ledger_index_uint32": { + "name": "credential_events_ledger_index_uint32", + "value": "\"credential_events\".\"ledger_index\" BETWEEN 0 AND 4294967295" + }, + "credential_events_transaction_index": { + "name": "credential_events_transaction_index", + "value": "\"credential_events\".\"transaction_index\" >= 0" + }, + "credential_events_expiration_uint32": { + "name": "credential_events_expiration_uint32", + "value": "\"credential_events\".\"expiration\" IS NULL OR \"credential_events\".\"expiration\" BETWEEN 0 AND 4294967295" + }, + "credential_events_type": { + "name": "credential_events_type", + "value": "\"credential_events\".\"event_type\" IN ('created', 'accepted', 'deleted')" + }, + "credential_events_delete_shape": { + "name": "credential_events_delete_shape", + "value": "(\"credential_events\".\"event_type\" = 'deleted' AND \"credential_events\".\"deletion_cause\" IS NOT NULL)\n OR (\"credential_events\".\"event_type\" <> 'deleted' AND \"credential_events\".\"deletion_cause\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_generations": { + "name": "credential_generations", + "schema": "", + "columns": { + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ledger_object_id": { + "name": "ledger_object_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_uid": { + "name": "schema_uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri_hex": { + "name": "uri_hex", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiration": { + "name": "expiration", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_ledger_index": { + "name": "created_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_transaction_index": { + "name": "created_transaction_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "last_ledger_index": { + "name": "last_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "deleted_ledger_index": { + "name": "deleted_ledger_index", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "deletion_cause": { + "name": "deletion_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_generations_live_uq": { + "name": "credential_generations_live_uq", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential_generations\".\"deleted_ledger_index\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_generations_exact_idx": { + "name": "credential_generations_exact_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schema_uid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_generations_stats_idx": { + "name": "credential_generations_stats_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_ledger_index", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expiration", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_generations_profile_fk": { + "name": "credential_generations_profile_fk", + "tableFrom": "credential_generations", + "tableTo": "network_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "profile_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "credential_generations_schema_fk": { + "name": "credential_generations_schema_fk", + "tableFrom": "credential_generations", + "tableTo": "schemas", + "columnsFrom": [ + "profile_id", + "schema_uid" + ], + "columnsTo": [ + "profile_id", + "schema_uid" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_generations_pk": { + "name": "credential_generations_pk", + "columns": [ + "profile_id", + "generation_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_generations_id": { + "name": "credential_generations_id", + "value": "\"credential_generations\".\"generation_id\" ~ '^[0-9a-f]{64}$'" + }, + "credential_generations_object": { + "name": "credential_generations_object", + "value": "\"credential_generations\".\"ledger_object_id\" ~ '^[0-9a-f]{64}$'" + }, + "credential_generations_schema": { + "name": "credential_generations_schema", + "value": "\"credential_generations\".\"schema_uid\" ~ '^[0-9a-f]{64}$'" + }, + "credential_generations_expiration_uint32": { + "name": "credential_generations_expiration_uint32", + "value": "\"credential_generations\".\"expiration\" IS NULL OR \"credential_generations\".\"expiration\" BETWEEN 0 AND 4294967295" + }, + "credential_generations_created_ledger_uint32": { + "name": "credential_generations_created_ledger_uint32", + "value": "\"credential_generations\".\"created_ledger_index\" BETWEEN 0 AND 4294967295" + }, + "credential_generations_created_transaction_index": { + "name": "credential_generations_created_transaction_index", + "value": "\"credential_generations\".\"created_transaction_index\" >= 0" + }, + "credential_generations_last_ledger_uint32": { + "name": "credential_generations_last_ledger_uint32", + "value": "\"credential_generations\".\"last_ledger_index\" BETWEEN 0 AND 4294967295" + }, + "credential_generations_deleted_ledger_uint32": { + "name": "credential_generations_deleted_ledger_uint32", + "value": "\"credential_generations\".\"deleted_ledger_index\" IS NULL OR \"credential_generations\".\"deleted_ledger_index\" BETWEEN 0 AND 4294967295" + }, + "credential_generations_ledger_order": { + "name": "credential_generations_ledger_order", + "value": "\"credential_generations\".\"last_ledger_index\" >= \"credential_generations\".\"created_ledger_index\"\n AND (\"credential_generations\".\"deleted_ledger_index\" IS NULL OR \"credential_generations\".\"deleted_ledger_index\" = \"credential_generations\".\"last_ledger_index\")" + }, + "credential_generations_deletion": { + "name": "credential_generations_deletion", + "value": "(\"credential_generations\".\"deleted_ledger_index\" IS NULL AND \"credential_generations\".\"deletion_cause\" IS NULL)\n OR (\"credential_generations\".\"deleted_ledger_index\" IS NOT NULL AND \"credential_generations\".\"deletion_cause\" IS NOT NULL)" + }, + "credential_generations_deletion_cause": { + "name": "credential_generations_deletion_cause", + "value": "\"credential_generations\".\"deletion_cause\" IS NULL OR \"credential_generations\".\"deletion_cause\" IN ('issuer_revoked', 'subject_rejected', 'subject_removed', 'expired_cleanup', 'account_deleted', 'self_deleted')" + } + }, + "isRLSEnabled": false + }, + "public.demo_pins": { + "name": "demo_pins", + "schema": "", + "columns": { + "pin_id": { + "name": "pin_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "challenge_id": { + "name": "challenge_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_ip_hash": { + "name": "requester_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cid": { + "name": "cid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_length": { + "name": "byte_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "unpinned_at": { + "name": "unpinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "demo_pins_wallet_quota_idx": { + "name": "demo_pins_wallet_quota_idx", + "columns": [ + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "demo_pins_ip_quota_idx": { + "name": "demo_pins_ip_quota_idx", + "columns": [ + { + "expression": "requester_ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "demo_pins_expiry_idx": { + "name": "demo_pins_expiry_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "demo_pins_cid_active_idx": { + "name": "demo_pins_cid_active_idx", + "columns": [ + { + "expression": "cid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "demo_pins_challenge_id_pin_challenges_challenge_id_fk": { + "name": "demo_pins_challenge_id_pin_challenges_challenge_id_fk", + "tableFrom": "demo_pins", + "tableTo": "pin_challenges", + "columnsFrom": [ + "challenge_id" + ], + "columnsTo": [ + "challenge_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "demo_pins_profile_id_network_profiles_profile_id_fk": { + "name": "demo_pins_profile_id_network_profiles_profile_id_fk", + "tableFrom": "demo_pins", + "tableTo": "network_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "profile_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "demo_pins_challenge_id_unique": { + "name": "demo_pins_challenge_id_unique", + "nullsNotDistinct": false, + "columns": [ + "challenge_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "demo_pins_id": { + "name": "demo_pins_id", + "value": "\"demo_pins\".\"pin_id\" ~ '^[0-9a-f]{64}$'" + }, + "demo_pins_ip_hash": { + "name": "demo_pins_ip_hash", + "value": "\"demo_pins\".\"requester_ip_hash\" ~ '^[0-9a-f]{64}$'" + }, + "demo_pins_wallet": { + "name": "demo_pins_wallet", + "value": "\"demo_pins\".\"wallet\" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'" + }, + "demo_pins_cid": { + "name": "demo_pins_cid", + "value": "\"demo_pins\".\"cid\" ~ '^b[a-z2-7]+$'" + }, + "demo_pins_byte_length": { + "name": "demo_pins_byte_length", + "value": "\"demo_pins\".\"byte_length\" BETWEEN 1 AND 65536" + }, + "demo_pins_status": { + "name": "demo_pins_status", + "value": "\"demo_pins\".\"status\" IN ('pending', 'pinned', 'failed', 'unpinned')" + }, + "demo_pins_failure_shape": { + "name": "demo_pins_failure_shape", + "value": "(\"demo_pins\".\"status\" = 'failed' AND \"demo_pins\".\"failure_code\" IS NOT NULL)\n OR (\"demo_pins\".\"status\" <> 'failed' AND \"demo_pins\".\"failure_code\" IS NULL)" + }, + "demo_pins_unpinned_shape": { + "name": "demo_pins_unpinned_shape", + "value": "(\"demo_pins\".\"status\" = 'unpinned' AND \"demo_pins\".\"unpinned_at\" IS NOT NULL)\n OR (\"demo_pins\".\"status\" <> 'unpinned' AND \"demo_pins\".\"unpinned_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.pin_challenges": { + "name": "pin_challenges", + "schema": "", + "columns": { + "challenge_id": { + "name": "challenge_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wallet": { + "name": "wallet", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_ip_hash": { + "name": "requester_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pin_challenges_wallet_created_idx": { + "name": "pin_challenges_wallet_created_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "wallet", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pin_challenges_ip_created_idx": { + "name": "pin_challenges_ip_created_idx", + "columns": [ + { + "expression": "requester_ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pin_challenges_expiry_idx": { + "name": "pin_challenges_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pin_challenges_profile_id_network_profiles_profile_id_fk": { + "name": "pin_challenges_profile_id_network_profiles_profile_id_fk", + "tableFrom": "pin_challenges", + "tableTo": "network_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "profile_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pin_challenges_id": { + "name": "pin_challenges_id", + "value": "\"pin_challenges\".\"challenge_id\" ~ '^[0-9a-f]{64}$'" + }, + "pin_challenges_ip_hash": { + "name": "pin_challenges_ip_hash", + "value": "\"pin_challenges\".\"requester_ip_hash\" ~ '^[0-9a-f]{64}$'" + }, + "pin_challenges_wallet": { + "name": "pin_challenges_wallet", + "value": "\"pin_challenges\".\"wallet\" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'" + }, + "pin_challenges_expiry": { + "name": "pin_challenges_expiry", + "value": "\"pin_challenges\".\"expires_at\" > \"pin_challenges\".\"created_at\"" + } + }, + "isRLSEnabled": false + }, + "public.hosted_payload_publications": { + "name": "hosted_payload_publications", + "schema": "", + "columns": { + "transaction_hash": { + "name": "transaction_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "locator": { + "name": "locator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_uid": { + "name": "schema_uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_ip_hash": { + "name": "requester_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hosted_payload_publications_wallet_quota_idx": { + "name": "hosted_payload_publications_wallet_quota_idx", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hosted_payload_publications_ip_quota_idx": { + "name": "hosted_payload_publications_ip_quota_idx", + "columns": [ + { + "expression": "requester_ip_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hosted_payload_publications_locator_idx": { + "name": "hosted_payload_publications_locator_idx", + "columns": [ + { + "expression": "locator", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hosted_payload_publications_locator_hosted_payloads_locator_fk": { + "name": "hosted_payload_publications_locator_hosted_payloads_locator_fk", + "tableFrom": "hosted_payload_publications", + "tableTo": "hosted_payloads", + "columnsFrom": [ + "locator" + ], + "columnsTo": [ + "locator" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "hosted_payload_publications_profile_id_network_profiles_profile_id_fk": { + "name": "hosted_payload_publications_profile_id_network_profiles_profile_id_fk", + "tableFrom": "hosted_payload_publications", + "tableTo": "network_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "profile_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "hosted_payload_publications_schema_fk": { + "name": "hosted_payload_publications_schema_fk", + "tableFrom": "hosted_payload_publications", + "tableTo": "schemas", + "columnsFrom": [ + "profile_id", + "schema_uid" + ], + "columnsTo": [ + "profile_id", + "schema_uid" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "hosted_payload_publications_tx_hash": { + "name": "hosted_payload_publications_tx_hash", + "value": "\"hosted_payload_publications\".\"transaction_hash\" ~ '^[0-9a-f]{64}$'" + }, + "hosted_payload_publications_issuer": { + "name": "hosted_payload_publications_issuer", + "value": "\"hosted_payload_publications\".\"issuer\" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'" + }, + "hosted_payload_publications_subject": { + "name": "hosted_payload_publications_subject", + "value": "\"hosted_payload_publications\".\"subject\" ~ '^r[1-9A-HJ-NP-Za-km-z]{24,34}$'" + }, + "hosted_payload_publications_schema": { + "name": "hosted_payload_publications_schema", + "value": "\"hosted_payload_publications\".\"schema_uid\" ~ '^[0-9a-f]{64}$'" + }, + "hosted_payload_publications_ip_hash": { + "name": "hosted_payload_publications_ip_hash", + "value": "\"hosted_payload_publications\".\"requester_ip_hash\" ~ '^[0-9a-f]{64}$'" + } + }, + "isRLSEnabled": false + }, + "public.hosted_payloads": { + "name": "hosted_payloads", + "schema": "", + "columns": { + "locator": { + "name": "locator", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "digest_hex": { + "name": "digest_hex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "hosted_payloads_locator": { + "name": "hosted_payloads_locator", + "value": "\"hosted_payloads\".\"locator\" ~ '^(?:[0-9a-f]{18}|[0-9a-f]{20})$'" + }, + "hosted_payloads_digest": { + "name": "hosted_payloads_digest", + "value": "\"hosted_payloads\".\"digest_hex\" ~ '^[0-9a-f]{64}$'" + }, + "hosted_payloads_size": { + "name": "hosted_payloads_size", + "value": "octet_length(\"hosted_payloads\".\"content\") BETWEEN 1 AND 65536" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index a0f74e0..f55146d 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -8,6 +8,20 @@ "when": 1788476248914, "tag": "0000_baseline", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1788803750924, + "tag": "0001_hosted_payloads", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1788959680306, + "tag": "0002_hosted_payload_locator_compatibility", + "breakpoints": true } ] } diff --git a/packages/db/src/provision.ts b/packages/db/src/provision.ts index 2c26d9d..9851216 100644 --- a/packages/db/src/provision.ts +++ b/packages/db/src/provision.ts @@ -190,6 +190,7 @@ const GRANT_RUNTIME_ACCESS_SQL = ` schema_events, schemas, credential_generations, credential_events TO xcs_api; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE pin_challenges, demo_pins TO xcs_api; + GRANT SELECT, INSERT ON TABLE hosted_payloads, hosted_payload_publications TO xcs_api; ` export async function provisionRuntimeDatabaseRoles( diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 5668f47..d8b49b5 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -3,3 +3,4 @@ export * from './indexer.js' export * from './catalog.js' export * from './credentials.js' export * from './pinning.js' +export * from './payloads.js' diff --git a/packages/db/src/schema/payloads.ts b/packages/db/src/schema/payloads.ts new file mode 100644 index 0000000..fec6fe6 --- /dev/null +++ b/packages/db/src/schema/payloads.ts @@ -0,0 +1,51 @@ +import { HASH_PATTERN, ADDRESS_PATTERN } from './common.js' +import { schemas } from './catalog.js' +import { networkProfiles } from './profiles.js' +import { sql } from 'drizzle-orm' +import { check, foreignKey, index, pgTable, text, timestamp } from 'drizzle-orm/pg-core' +export const hostedPayloads = pgTable( + 'hosted_payloads', + { + locator: text('locator').primaryKey(), + digestHex: text('digest_hex').notNull(), + content: text('content').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [ + check('hosted_payloads_locator', sql`${table.locator} ~ '^(?:[0-9a-f]{18}|[0-9a-f]{20})$'`), + check('hosted_payloads_digest', sql`${table.digestHex} ~ ${HASH_PATTERN}`), + check('hosted_payloads_size', sql`octet_length(${table.content}) BETWEEN 1 AND 65536`), + ], +) +export const hostedPayloadPublications = pgTable( + 'hosted_payload_publications', + { + transactionHash: text('transaction_hash').primaryKey(), + locator: text('locator') + .notNull() + .references(() => hostedPayloads.locator, { onDelete: 'restrict' }), + profileId: text('profile_id') + .notNull() + .references(() => networkProfiles.profileId, { onDelete: 'restrict' }), + issuer: text('issuer').notNull(), + subject: text('subject').notNull(), + schemaUid: text('schema_uid').notNull(), + requesterIpHash: text('requester_ip_hash').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [ + foreignKey({ + name: 'hosted_payload_publications_schema_fk', + columns: [table.profileId, table.schemaUid], + foreignColumns: [schemas.profileId, schemas.schemaUid], + }).onDelete('restrict'), + index('hosted_payload_publications_wallet_quota_idx').on(table.issuer, table.createdAt), + index('hosted_payload_publications_ip_quota_idx').on(table.requesterIpHash, table.createdAt), + index('hosted_payload_publications_locator_idx').on(table.locator), + check('hosted_payload_publications_tx_hash', sql`${table.transactionHash} ~ ${HASH_PATTERN}`), + check('hosted_payload_publications_issuer', sql`${table.issuer} ~ ${ADDRESS_PATTERN}`), + check('hosted_payload_publications_subject', sql`${table.subject} ~ ${ADDRESS_PATTERN}`), + check('hosted_payload_publications_schema', sql`${table.schemaUid} ~ ${HASH_PATTERN}`), + check('hosted_payload_publications_ip_hash', sql`${table.requesterIpHash} ~ ${HASH_PATTERN}`), + ], +) diff --git a/packages/db/test/schema.test.ts b/packages/db/test/schema.test.ts index bc6f30e..15e670f 100644 --- a/packages/db/test/schema.test.ts +++ b/packages/db/test/schema.test.ts @@ -111,13 +111,21 @@ describe('database schema', () => { expect(indexerStatuses.leaseExpiresAt.notNull).toBe(false) }) - it('ships one generated baseline instead of pre-production migration history', () => { + it('preserves the deployed baseline and additive hosted payload migrations', () => { const migrationFiles = readdirSync(DRIZZLE_DIRECTORY).filter((name) => name.endsWith('.sql')) const snapshotFiles = readdirSync(new URL('meta/', DRIZZLE_DIRECTORY)).filter((name) => name.endsWith('_snapshot.json'), ) - expect(migrationFiles).toEqual(['0000_baseline.sql']) - expect(snapshotFiles).toEqual(['0000_snapshot.json']) + expect(migrationFiles).toEqual([ + '0000_baseline.sql', + '0001_hosted_payloads.sql', + '0002_hosted_payload_locator_compatibility.sql', + ]) + expect(snapshotFiles).toEqual([ + '0000_snapshot.json', + '0001_snapshot.json', + '0002_snapshot.json', + ]) const baseline = readFileSync(new URL('0000_baseline.sql', DRIZZLE_DIRECTORY), 'utf8') expect(baseline).toContain('CREATE TABLE "ledger_checkpoints"') @@ -129,6 +137,10 @@ describe('database schema', () => { const journal = JSON.parse( readFileSync(new URL('meta/_journal.json', DRIZZLE_DIRECTORY), 'utf8'), ) as { entries: Array<{ idx: number; tag: string }> } - expect(journal.entries).toEqual([expect.objectContaining({ idx: 0, tag: '0000_baseline' })]) + expect(journal.entries).toEqual([ + expect.objectContaining({ idx: 0, tag: '0000_baseline' }), + expect.objectContaining({ idx: 1, tag: '0001_hosted_payloads' }), + expect.objectContaining({ idx: 2, tag: '0002_hosted_payload_locator_compatibility' }), + ]) }) }) From 022bf9d3ba4bd30ebb06e9a812ae5e9a0d2ed406 Mon Sep 17 00:00:00 2001 From: Sanka Date: Wed, 23 Sep 2026 16:44:12 +0200 Subject: [PATCH 06/41] feat(web): integrate wallet and credential flows with Nuxt UI --- AGENTS.md | 3 +- apps/web/README.md | 194 +++++++--- apps/web/app/app.vue | 11 + .../HostedPublicationRecovery.client.vue | 157 ++++++++ .../web/app/components/TransactionPreview.vue | 85 ++++- .../app/components/WalletButton.client.vue | 287 ++++++++++++++ apps/web/app/components/WalletButton.vue | 127 ------- .../app/composables/useLocalPayloadStore.ts | 60 +-- apps/web/app/composables/useWallet.ts | 343 ++++++++--------- apps/web/app/composables/useXcsApi.ts | 31 ++ apps/web/app/layouts/default.vue | 10 + apps/web/app/pages/accept.vue | 191 ++++++---- apps/web/app/pages/credentials/index.vue | 183 +++++++++ apps/web/app/pages/issue.vue | 350 +++++++++--------- apps/web/app/pages/operations.vue | 1 + apps/web/app/pages/revoke.vue | 13 +- apps/web/app/pages/schemas/register.vue | 56 ++- apps/web/app/plugins/wallet.client.ts | 36 +- apps/web/app/types/nuxt.d.ts | 3 - apps/web/app/utils/browserE2eHarness.ts | 114 +++--- apps/web/app/utils/credentialInbox.ts | 208 +++++++++++ apps/web/app/utils/credentialReview.ts | 3 +- apps/web/app/utils/crossmarkSession.ts | 128 +++++++ apps/web/app/utils/developerQuickstart.ts | 4 +- apps/web/app/utils/gemWalletRawSigning.ts | 83 +++++ apps/web/app/utils/hostedPayload.ts | 138 +++++++ apps/web/app/utils/hostedPublicationQueue.ts | 166 +++++++++ apps/web/app/utils/localPayloadStore.ts | 10 +- apps/web/app/utils/operationJournal.ts | 58 ++- apps/web/app/utils/payloadPublication.ts | 22 +- apps/web/app/utils/walletAdapters.ts | 261 +------------ apps/web/app/utils/walletCompatibility.ts | 57 ++- apps/web/app/utils/walletConnection.ts | 130 +++++++ apps/web/app/utils/walletDiscovery.ts | 44 +++ apps/web/app/utils/walletSubmission.ts | 14 + apps/web/e2e/crossmark.production.spec.ts | 194 ++++++++++ apps/web/e2e/developers.spec.ts | 8 +- apps/web/e2e/pilot.spec.ts | 336 ++++++++++------- apps/web/e2e/security.production.spec.ts | 11 + apps/web/e2e/wallet-menu.spec.ts | 42 ++- apps/web/i18n/locales/en.json | 104 +++++- apps/web/i18n/locales/fr.json | 104 +++++- apps/web/nuxt.config.ts | 20 +- apps/web/package.json | 3 +- apps/web/playwright.config.ts | 2 +- apps/web/playwright.crossmark.config.ts | 19 + .../routes/__e2e-api/v1/[...path].get.ts | 3 +- .../server/routes/__e2e-api/v1/verify.post.ts | 3 +- apps/web/test/credentialReview.test.ts | 21 ++ apps/web/test/crossmarkNetwork.test.ts | 127 +++++++ apps/web/test/crossmarkSession.test.ts | 199 ++++++++++ apps/web/test/gemWalletRawSigning.test.ts | 153 ++++++++ apps/web/test/hostedPublicationQueue.test.ts | 210 +++++++++++ apps/web/test/walletAdapters.test.ts | 137 +------ apps/web/test/walletCompatibility.test.ts | 71 ++-- apps/web/test/walletConnection.test.ts | 226 +++++++++++ apps/web/test/walletDiscovery.test.ts | 59 +++ apps/web/test/walletSubmission.test.ts | 104 ++++++ .../test/xamanAdapterNetworkRefresh.test.ts | 282 +------------- docs/runbooks/gemwallet-raw-signing.md | 69 ++++ docs/runbooks/otsu-live-flow.md | 114 ++++++ patches/README.md | 26 ++ patches/xrpl-connect@1.0.0-rc.0.patch | 79 ---- patches/xrpl-connect@1.0.0-rc.2.patch | 59 +++ pnpm-lock.yaml | 41 +- pnpm-workspace.yaml | 2 +- 66 files changed, 4689 insertions(+), 1720 deletions(-) create mode 100644 apps/web/app/components/HostedPublicationRecovery.client.vue create mode 100644 apps/web/app/components/WalletButton.client.vue delete mode 100644 apps/web/app/components/WalletButton.vue create mode 100644 apps/web/app/pages/credentials/index.vue create mode 100644 apps/web/app/utils/credentialInbox.ts create mode 100644 apps/web/app/utils/crossmarkSession.ts create mode 100644 apps/web/app/utils/gemWalletRawSigning.ts create mode 100644 apps/web/app/utils/hostedPayload.ts create mode 100644 apps/web/app/utils/hostedPublicationQueue.ts create mode 100644 apps/web/app/utils/walletConnection.ts create mode 100644 apps/web/app/utils/walletDiscovery.ts create mode 100644 apps/web/e2e/crossmark.production.spec.ts create mode 100644 apps/web/playwright.crossmark.config.ts create mode 100644 apps/web/test/crossmarkNetwork.test.ts create mode 100644 apps/web/test/crossmarkSession.test.ts create mode 100644 apps/web/test/gemWalletRawSigning.test.ts create mode 100644 apps/web/test/hostedPublicationQueue.test.ts create mode 100644 apps/web/test/walletConnection.test.ts create mode 100644 apps/web/test/walletDiscovery.test.ts create mode 100644 docs/runbooks/gemwallet-raw-signing.md create mode 100644 docs/runbooks/otsu-live-flow.md create mode 100644 patches/README.md delete mode 100644 patches/xrpl-connect@1.0.0-rc.0.patch create mode 100644 patches/xrpl-connect@1.0.0-rc.2.patch diff --git a/AGENTS.md b/AGENTS.md index 968894b..c4d9de3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,9 +2,10 @@ - Before pushing Compose changes, render the exact overlays with every referenced profile and, when a Docker engine is available, run the production image build plus secret-file startup under the configured unprivileged users; configuration rendering alone does not validate build context or bind-mounted secret readability. - When cryptographically checking an XRPL transaction returned by `tx`, request `binary: true` and verify the canonical `tx_blob`; API v2 `tx_json` can rename serialized fields (for example, `Amount` is exposed as `DeliverMax`) and is not the canonical object to re-encode for signature verification. -- For Playwright smoke assertions, inspect the rendered semantics and use a unique role, test ID, or explicitly scoped locator; do not assume localized headings or repeated public identifiers resolve to exactly one element in strict mode. +- For Playwright smoke assertions, inspect the rendered semantics and use a unique role, test ID, or explicitly scoped locator; scope navigation links to the named main-navigation region because the footer is also a navigation region and repeats them. Do not assume localized headings or repeated public identifiers resolve to exactly one element in strict mode. For wallet buttons whose CSS changes letter case, use an anchored case-insensitive accessible-name match after inspecting the current screen, not the casing returned by textContent. - When running a second Nuxt dev or Playwright web-server instance from another worktree, set `NUXT_IGNORE_LOCK=1` and use a distinct port; Nuxt's development lock is shared across worktrees. - After editing Prettier-managed files, format the touched files before running format checks; do not use the check command as the formatter. - For one-off Node scripts, resolve dependencies from the owning workspace package; running from the monorepo root can silently select an incompatible ancestor `node_modules` package. - Before validating web changes that depend on `@xcs-protocol/core` or `@xcs-protocol/sdk`, rebuild the changed workspace package and keep Nuxt/Vite's forced fresh pre-bundle enabled so cached code cannot mask the current `dist` output. - Quote shell paths that contain route brackets or other glob metacharacters before passing them to commands such as `sed` or `rg`. +- For wallet connection regressions, derive SignIn responses, live queries and initialization events separately from the installed extension's public code, then test connection, account refresh and pre-signature checks through the real SDK—not just the connected button. Do not assume they share one state source or omit fields returned with approval. Treat account/network notifications as a request to recheck identity, not proof that an approved session changed. diff --git a/apps/web/README.md b/apps/web/README.md index 00e1f42..cf0f59e 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -31,18 +31,27 @@ the public submission RPC or the diagnostic status DTO. All valid permissionless schemas are public and discoverable. Credential verification is deliberately exact: callers share a generation ID, transaction hash or the complete issuer/subject/schema tuple. The site has no subject feed, account-wide Credential enumeration or -claims search, and it displays no Commons issuer badge or universal trust result. Aggregate -statistics contain only ledger-derived metadata. See +claims search served by Commons, and it displays no Commons issuer badge or universal trust result. +The connected-wallet `/credentials` page is a local self-view: the browser reads raw Credential +objects directly from the configured public XRPL RPC, then reconciles each XCS-shaped tuple through +the existing exact API route. It does not fetch claims or add a Commons-served account-wide +Credential list. The RPC provider can observe the queried address and browser network metadata. +Aggregate statistics contain only ledger-derived metadata. See [`ADR 0002`](../../docs/adr/0002-public-product-and-discovery.md). ## Implemented site map +English is the default, unprefixed locale. French remains available under `/fr` from the language +selector. + The current application organizes the existing workflows as follows: - **Landing and Explorer:** `/` presents the editorial Testnet landing page, aggregate checkpoint statistics and exact search. `/schemas` and `/schemas/:uid` provide paginated schema discovery, `/activity` shows schema registrations only, `/credentials/:generationId` and - `/transactions/:hash` expose exact evidence, and `/status` shows the indexer/network view; + `/transactions/:hash` expose exact evidence, `/credentials` lists unaccepted credentials for the + currently connected wallet through direct XRPL discovery, and `/status` shows the indexer/network + view; - **Create:** `/studio` is the single creation hub. It emphasizes schema registration and single-credential issuance, then links acceptance, revocation and local operation recovery. Schema registration has a guided scalar-field editor, course-completion and diploma templates, @@ -64,15 +73,55 @@ invited to turn shared coordinates into a secondary public directory. ## Wallet support -The web app pins `xrpl-connect@1.0.0-rc.0` and registers its eight official adapters: Xaman, -Crossmark, GemWallet, WalletConnect, Ledger, Xyra, Otsu and MetaMask Snap. Crossmark, GemWallet, -Ledger, Xyra, Otsu and MetaMask Snap are registered without deployment-specific configuration. -Xaman is added only when `NUXT_PUBLIC_XAMAN_API_KEY` is set, and WalletConnect only when -`NUXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` is set. Both values identify a public application to its -wallet provider; they are browser-visible identifiers, not secrets. One Xaman application key is -used by every visitor of that XCS deployment. Register the deployment origin with a trailing slash -as an exact redirect URI in the Xaman Developer Console. XCS uses that stable root callback from -every page; it never uses the current route as the OAuth callback. +The chooser shows the pending wallet name while approval and network confirmation are outstanding. +Connection errors stay at the top of the scrollable chooser. If the wallet reports another +network, select XRP Ledger Testnet and retry; an existing app permission in the extension does not +by itself establish the site's current session. Reloading the page requires connecting again +because automatic reconnection is disabled. +A connection must finish within 90 seconds; **Cancel connection** invalidates the attempt immediately, +so a late extension response cannot reconnect it. Once approved, the header shows the wallet name, +address, network and an explicit **Disconnect** action. Rejection and timeout errors remain visible. +The open chooser updates when Crossmark emits its real provider-detection event; it does not require +closing and reopening the menu. Account refresh before preparation/signing has a 10-second deadline. +A timeout invalidates the session immediately, even if extension teardown hangs; late replies cannot +restore the old account. + +The rc.2 Crossmark adapter does not forward session-change events. XCS listens to the official +Crossmark SDK's `signout`, `user-change` and `network-change` events. Signout clears the connection; +user/network notifications trigger an authoritative account refresh. Duplicate initialization +notifications retain the approved session when its account and network are unchanged. Changed or +unverifiable sessions are disconnected with an explanation. Pending sign-in events and other wallets +are left alone. This does not replace the account/network and signature checks at submission. + +`pnpm exec playwright test --config playwright.crossmark.config.ts` checks the full production +connection path against `https://localhost:3443` (override with `XCS_WALLET_TEST_URL`). The test uses +an isolated browser with a fixture of Crossmark's extension messages, including response envelopes +and duplicate initialization notifications; it does not replace SDK methods or modify the deployed +site. Set `XCS_BROWSER_EXECUTABLE` to Brave's executable when using the macOS-trusted local CA. +Actual wallet approval still requires a manual extension test. + +The web app pins `xrpl-connect@1.0.0-rc.2` and +`@xrpl-commons/xrpl-connect-vue@1.0.0-rc.2`. The official Vue plugin owns the application-scoped +`WalletManager` and reactive account state. XCS uses its documented headless `connect()` API from a +small CSP-compatible chooser: the packaged `` loads Google Fonts and inline styles, +which do not satisfy XCS's current browser policy. XCS does not subclass wallet adapters. +One versioned pnpm patch fixes the rc.2 ESM Crossmark adapter used by this application: +it validates the network returned with the fresh SignIn approval instead of discarding it +and reading Crossmark's separately hydrated background replica. Older responses that omit +the network still require the live query. A present but incomplete network is rejected; +Mainnet never becomes Testnet. Account refresh and pre-signature checks validate the documented +SDK session network, updated by approval and network-change events. The raw background getter +is stale in Crossmark 0.2.19; it is not used as a second network gate. Missing/malformed session +data still fails closed, without falling back to the adapter's previous network. See +[`patches/README.md`](../../patches/README.md) for scope and removal criteria. +It registers Xaman, Crossmark, GemWallet, WalletConnect, Ledger, Xyra, Otsu and MetaMask Snap. Xaman is added only when `NUXT_PUBLIC_XAMAN_API_KEY` is set, and +WalletConnect only when `NUXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` is set. Both values identify a +public application to its wallet provider; they are browser-visible identifiers, not secrets. + +For local testing, keep Otsu and Crossmark in separate browser profiles. The inspected Otsu +unpacked extension locks `window.xrpl` and `window.crossmark`; Crossmark 0.2.19 also writes +those globals. Co-installation can prevent Crossmark's provider from initializing. This is +an extension collision, not something the application should hide by faking availability. Public deployment is additionally blocked on third-party license review. The RC bundle contains WalletConnect code under the WalletConnect Community License, and its GemWallet dependency requires @@ -82,23 +131,24 @@ dependencies would only hide the same bundled code from the license scanner; it workaround. The upstream adapter packages are not yet published separately, so there is currently no npm-supported way to retain all eight adapters while excluding only those two integrations. -The workspace applies `patches/xrpl-connect@1.0.0-rc.0.patch` while that release candidate is -pinned. Xaman's OAuth provider serializes `network_id` as a decimal string and may omit optional -network metadata from a later `ping()`, whereas the published adapter requires a complete -string/number pair. The patch accepts only canonical safe-integer IDs, preserves missing optional -metadata and rejects a network that contradicts the explicit transaction target. Because Xaman's -public browser SDK does not expose its supported OAuth `force_network` parameter, the XCS adapter -adds `force_network=TESTNET` only to the official Xaman authorization URL and evicts an older cached -XCS OAuth session when it cannot prove Testnet. The patch also accepts an absent or empty -`multisign_account` in a valid single-signature response: Xaman uses both forms for that -inapplicable field, while the release candidate incorrectly requires an explicit `null`. Every -signing payload separately carries `force_network: TESTNET`, and the adapter validates the resolved -signing network. The patch also retries the authoritative resolved-payload read for sign-only -requests: Xaman can report the signature before that payload is immediately readable, and the RC -otherwise abandons the operation before XCS can validate and submit the blob. Remove these -compatibility measures only after equivalent behavior ships in the pinned dependency. -After a sign request resolves, the XCS adapter closes the script-opened Xaman window when browser -policy allows it and restores focus to the application so submission progress remains visible. +`autoConnect` is disabled: an explicit selection in the XCS chooser is the only connection trigger, +so a cached Xaman session is never presented as a fresh user action. Xaman may replace +`LastLedgerSequence` during signing; XCS omits that volatile field from the Xaman request, then +cryptographically checks the complete returned blob and permits only the SDK's explicit +`LastLedgerSequence` refresh rule. The initial wallet network, active XCS profile and configured +XRPL RPC are all required to resolve to Testnet before submission. XCS does not use Xaman's later +optional `ping()` metadata as a second network gate. + +Otsu stores dApp permissions by origin and otherwise resolves a later `connect()` from the stored +address without presenting the wallet approval again. Before an explicit Otsu selection, XCS calls +the adapter's public `disconnect()` method to revoke that stale origin permission, then delegates the +new connection to XRPL Connect. This keeps the selected account bound to a fresh user-approved Otsu +session instead of silently reusing an older address. Otsu's extension service worker can retain the +public account while its signing key is locked. XRPL Connect rc.2 currently mislabels the resulting +Otsu `Account not found` failure as an uninstalled wallet. XCS rechecks the adapter's public +availability, reports the signing-account failure accurately and closes that unusable session. The +user must unlock Otsu and explicitly reconnect before retrying; XCS never accesses wallet keys or +repeats a rejected signing request automatically. XCS filters this surface to adapters that expose `sign()` and never calls `signAndSubmit`. A wallet may return a `tx_blob` or signed `tx_json`; the application normalizes the artifact, derives and @@ -109,9 +159,19 @@ before persistence or relay. Wallet capability is transaction-specific. GemWallet 3.8.x embeds an XRPL validator from before native XLS-70 Credentials: it can sign the `Payment` used for schema registration but rejects -`CredentialCreate`, `CredentialAccept` and `CredentialDelete` before signing. XCS therefore keeps -GemWallet available for schemas while blocking those three transactions early with -`WALLET_CREDENTIAL_TRANSACTION_UNSUPPORTED`. Xaman has native Credential handling in its current +`CredentialCreate`, `CredentialAccept` and `CredentialDelete` before signing. For these three types, +XCS offers an explicit raw-signing path. The full reviewed transaction remains visible, and a new +acknowledgement is required for every attempt: GemWallet displays a hex message, not decoded fields. +This is a transaction authorization, not a login signature. XCS uses `GemWalletAPI.signMessage(hex, +true)`, exported by XRPL Connect, because rc.2's generic message adapter discards the hex flag. +`xrpl.js` validates and serializes the exact prepared transaction, binds its public key to the +connected Testnet account, assembles the returned signature and verifies it through the same SDK +boundary before journaling or submission. No keys enter XCS, no automatic fallback follows a +rejection, and schema Payments still use the normal adapter. Raw signing currently supports master +keys only; delegated regular keys and multisigning are rejected rather than guessed. +See [the real GemWallet Testnet qualification](../../docs/runbooks/gemwallet-raw-signing.md) for +validated create/accept/delete transactions, popup limitations and local rollback. +Xaman has native Credential handling in its current source and is the preferred candidate when its public application identifier is configured, but it still requires the real-wallet Testnet gate below. Unknown adapters remain visible as unvalidated; if one emits the exact legacy `Invalid field TransactionType` error, XCS converts it to the same @@ -192,6 +252,51 @@ Private and public runtime switches must match exactly, and both the Nitro start plugin reject the mode outside a development bundle. `nuxt.config.ts` also rejects `XCS_BROWSER_E2E=1` when `NODE_ENV=production`. +## Schema text fields + +Schema names (1–64 UTF-8 bytes) and descriptions (1–256 UTF-8 bytes) are required +single-line protocol fields. The guided editor uses single-line inputs with byte +counters and field-specific validation messages. Accents and emoji can use several +bytes; the UI does not truncate or replace the submitted text. JSON mode uses the +same protocol checks and identifies the failing field instead of discarding its +error path. + +## Hosted HTTPS payloads + +Set `NUXT_PUBLIC_PAYLOAD_BASE_URL` to the API's exact public HTTPS payload origin and +`XCS_LOCAL_PAYLOAD_STORE=0`. Hosted issuance requires explicit public-storage consent. +Canonical payloads larger than **64 KiB of UTF-8 bytes** are rejected before preparation and again +before signing, matching the hosted API limit. External issuer-managed storage keeps its own limit. +The page builds the immutable URI, signs and submits `CredentialCreate`, waits for +ledger/indexer confirmation, then publishes the canonical bytes with the validated +signed blob. It checks the publication receipt and reads the HTTPS bytes back. +Before signing, a credential-specific HEAD request checks that the public host is +reachable. A 404 is expected before first publication; network/CORS failures or +server errors block signing. This does not prove future availability or successful +publication. Sharing/acceptance links appear only after exact-byte publication +verification succeeds, never just because the ledger transaction was validated. +Issuer-managed external HTTPS hosting still requires publication before signing. + +The consented public payload is saved in a per-job browser recovery queue before opening the wallet. +The validated signature is persisted before submission and linked to the operation journal. This +link also recovers an interrupted second storage write. Storage/quota failures block submission; +pending copies are never silently evicted. Up to 20 pending copies are retained until successful +publication or explicit removal. Do not clear browser data while recovery is needed. + +After a reload, **Issue** and **Operations** show pending publications. **Retry payload publication** +only uploads the same payload and signed transaction; it never signs or submits another credential. +The API still requires validated, indexed evidence. If submission timed out, reconcile the original +operation in Operations, then retry publication. Recovery is removed only after the receipt and +HTTPS bytes verify; terminal journal signatures are then purged. Damaged records are isolated and +can be explicitly removed without blocking valid jobs. An interrupted wallet approval with no +recorded signature requires checking the wallet/Operations first, not blindly issuing again. +Download the payload copy before removing an unfinished recovery record. Recovery is local to this +browser profile and origin, not a backup across devices; it uses no test payload-serving fallback. + +Do not use a rotating tunnel domain for durable credentials: the URI on XRPL cannot +follow a domain change. A tunnel can prove a disposable Testnet flow, but production +requires a fixed domain and persistent, backed-up storage. + ## Local browser payload store For manual Testnet flows without an issuer-controlled HTTPS host, start the development site with: @@ -259,7 +364,6 @@ NUXT_PUBLIC_API_BASE_URL=https://xcs-api.example NUXT_PUBLIC_RPC_URL=wss://s.altnet.rippletest.net:51233 NUXT_PUBLIC_PROFILE_ID=xrpl-testnet-xcs-v0.1 NUXT_PUBLIC_XAMAN_API_KEY=optional-public-xaman-application-id -NUXT_PUBLIC_XAMAN_REDIRECT_URL=https://xcs.example/ NUXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=optional-public-reown-project-id ``` @@ -281,12 +385,8 @@ profile must be returned by the API. `NUXT_PUBLIC_XAMAN_API_KEY` and `NUXT_PUBLIC_WALLET_CONNECT_PROJECT_ID` are optional public application identifiers. Omitting either variable removes only that adapter; it does not prevent the other six adapters from loading. Do not put a Xaman secret, WalletConnect relay secret, wallet key or -other credential in either value. `NUXT_PUBLIC_XAMAN_REDIRECT_URL` is optional and otherwise -defaults to the current application origin with a trailing slash. When set, it must be that exact -same-origin root URL; non-loopback deployments require HTTPS. Add the same exact URL to the Xaman -application's redirect allowlist. In Compose, set the corresponding operator variables -`XCS_PUBLIC_XAMAN_API_KEY`, `XCS_PUBLIC_XAMAN_REDIRECT_URL` and -`XCS_PUBLIC_WALLET_CONNECT_PROJECT_ID`. +other credential in either value. In Compose, set the corresponding operator variables +`XCS_PUBLIC_XAMAN_API_KEY` and `XCS_PUBLIC_WALLET_CONNECT_PROJECT_ID`. Each independently hosted XCS deployment needs its own Xaman public application key and registered origin. The Commons key is not a universal credential for arbitrary self-hosted domains. Follow @@ -308,12 +408,18 @@ public endpoint rather than either private indexer source. ## Pilot payload publication -The deployed Commons beta issuance and acceptance flows support issuer-hosted public HTTPS payloads -only; the loopback development aid documented above is intentionally not deployable. Commons does -not store, cache or search credential claims. The issuer payload host must +The deployed Commons beta issuance and acceptance flows support public HTTPS payloads, +whether issuer-hosted or published through the configured hosted service. The loopback +development aid documented above is intentionally not deployable. The payload host must allow the web origin through CORS and return `application/json` (or a `+json` media type). Acceptance -first displays only indexed metadata, the URI and its host. The subject must explicitly consent -before the browser contacts that host. Consent stays in memory and is bound to the displayed +first displays the issuer, recipient, schema name and status. URI, hashes and ledger +diagnostics are collapsed under **Technical details**. Checking **Load and verify these +details** grants consent and immediately starts verification, with loading/error feedback +and a retry action; it never opens the wallet. For an unknown issuer, the acknowledgement +becomes available after the content loads. Checking it prepares the transaction and shows +**Accept in wallet** next to the review; only clicking that button requests a signature. +Raw transaction fields remain available in a collapsed preview. The subject must explicitly +consent before the browser contacts the displayed host. Consent stays in memory and is bound to the displayed generation, exact URI and hostname; metadata is re-read and must still match before every payload request. Issuer trust remains a separate decision: an explicitly `untrusted` issuer is blocked, a `trusted` issuer needs no additional action, and the Commons-default `unknown` result requires a diff --git a/apps/web/app/app.vue b/apps/web/app/app.vue index a0b9d6b..a2aec3b 100644 --- a/apps/web/app/app.vue +++ b/apps/web/app/app.vue @@ -1,10 +1,21 @@