@learncard/network-brain-service 3.19.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +47 -82
- package/AGENTS.md +52 -0
- package/CHANGELOG.md +147 -0
- package/README.md +51 -2
- package/bootstrapLambda.test.ts +72 -0
- package/config/config.dev.json +29 -0
- package/config/config.example.json +29 -0
- package/config/config.json +1 -0
- package/config/config.production.json +29 -0
- package/config/config.scouts.dev.json +24 -0
- package/config/config.scouts.production.json +24 -0
- package/contractEventsLambda.ts +18 -0
- package/contractEventsLambdaApp.ts +19 -0
- package/didWebLambda.ts +13 -18
- package/didWebLambdaApp.ts +22 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
- package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
- package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
- package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
- package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
- package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.d.ts +2 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
- package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
- package/dist/accesslayer/presentation/read.d.ts.map +1 -1
- package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
- package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/share-link/abandon.d.ts +3 -0
- package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
- package/dist/accesslayer/share-link/attachment.d.ts +37 -0
- package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
- package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
- package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
- package/dist/accesslayer/share-link/errors.d.ts +8 -0
- package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
- package/dist/accesslayer/share-link/finalize.d.ts +3 -0
- package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
- package/dist/accesslayer/share-link/helpers.d.ts +59 -0
- package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
- package/dist/accesslayer/share-link/index.d.ts +12 -0
- package/dist/accesslayer/share-link/index.d.ts.map +1 -0
- package/dist/accesslayer/share-link/list.d.ts +5 -0
- package/dist/accesslayer/share-link/list.d.ts.map +1 -0
- package/dist/accesslayer/share-link/read.d.ts +5 -0
- package/dist/accesslayer/share-link/read.d.ts.map +1 -0
- package/dist/accesslayer/share-link/receipt.d.ts +11 -0
- package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
- package/dist/accesslayer/share-link/recovery.d.ts +6 -0
- package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
- package/dist/accesslayer/share-link/reserve.d.ts +4 -0
- package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
- package/dist/accesslayer/share-link/revoke.d.ts +3 -0
- package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
- package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
- package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
- package/dist/accesslayer/share-link/transaction.d.ts +15 -0
- package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
- package/dist/accesslayer/share-link/types.d.ts +375 -0
- package/dist/accesslayer/share-link/types.d.ts.map +1 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +49 -0
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/config/applyDockerStageConfig.d.ts +2 -0
- package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
- package/dist/config/environment.d.ts +14 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
- package/dist/config/stageConfig.d.ts +112 -0
- package/dist/config/stageConfig.d.ts.map +1 -0
- package/dist/config/stageConfig.test.d.ts +2 -0
- package/dist/config/stageConfig.test.d.ts.map +1 -0
- package/dist/constants/auth-grant.d.ts +2 -0
- package/dist/constants/auth-grant.d.ts.map +1 -1
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/docker-entry.d.ts.map +1 -1
- package/dist/helpers/app-manifest.helpers.d.ts +5 -0
- package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
- package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
- package/dist/helpers/consent-referral.helpers.d.ts +2 -0
- package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
- package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
- package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-events.helpers.d.ts +25 -0
- package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-requests.helpers.d.ts +19 -0
- package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
- package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
- package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
- package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
- package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
- package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
- package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inline-template.helpers.d.ts +45 -0
- package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
- package/dist/helpers/notificationMessages.d.ts +1 -1
- package/dist/helpers/notificationMessages.d.ts.map +1 -1
- package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts +6 -0
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/presentation-metadata.d.ts +4 -0
- package/dist/helpers/presentation-metadata.d.ts.map +1 -0
- package/dist/helpers/presentation.helpers.d.ts +1 -1
- package/dist/helpers/presentation.helpers.d.ts.map +1 -1
- package/dist/helpers/profile.helpers.d.ts +2 -0
- package/dist/helpers/profile.helpers.d.ts.map +1 -1
- package/dist/helpers/share-content-client/adapters.d.ts +10 -0
- package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
- package/dist/helpers/share-content-client/canonical.d.ts +7 -0
- package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
- package/dist/helpers/share-content-client/client.d.ts +5 -0
- package/dist/helpers/share-content-client/client.d.ts.map +1 -0
- package/dist/helpers/share-content-client/config.d.ts +11 -0
- package/dist/helpers/share-content-client/config.d.ts.map +1 -0
- package/dist/helpers/share-content-client/index.d.ts +6 -0
- package/dist/helpers/share-content-client/index.d.ts.map +1 -0
- package/dist/helpers/share-content-client/responses.d.ts +16 -0
- package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
- package/dist/helpers/share-content-client/types.d.ts +145 -0
- package/dist/helpers/share-content-client/types.d.ts.map +1 -0
- package/dist/helpers/share-link-attachment-access.d.ts +4 -0
- package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
- package/dist/helpers/share-link-attachment.d.ts +18 -0
- package/dist/helpers/share-link-attachment.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
- package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
- package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
- package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
- package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
- package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
- package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
- package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
- package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
- package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
- package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
- package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
- package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts +20 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts +38 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
- package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
- package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/types.d.ts +56 -0
- package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/config.d.ts +16 -0
- package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
- package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
- package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
- package/dist/helpers/share-link-owner-projection.d.ts +4 -0
- package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
- package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
- package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
- package/dist/helpers/share-link-passcode.d.ts +6 -0
- package/dist/helpers/share-link-passcode.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/index.d.ts +4 -0
- package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/production.d.ts +11 -0
- package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
- package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/types.d.ts +22 -0
- package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
- package/dist/helpers/share-link-public/config.d.ts +13 -0
- package/dist/helpers/share-link-public/config.d.ts.map +1 -0
- package/dist/helpers/share-link-view-notification.d.ts +2 -0
- package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
- package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
- package/dist/helpers/smartResume.helpers.d.ts +3 -0
- package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AppManifestVersion.d.ts +20 -0
- package/dist/models/AppManifestVersion.d.ts.map +1 -0
- package/dist/models/AppStoreListing.d.ts +13 -0
- package/dist/models/AppStoreListing.d.ts.map +1 -1
- package/dist/models/ConsentFlowContract.d.ts +13 -2
- package/dist/models/ConsentFlowContract.d.ts.map +1 -1
- package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
- package/dist/models/Integration.d.ts +2 -0
- package/dist/models/Integration.d.ts.map +1 -1
- package/dist/models/Presentation.d.ts +2 -0
- package/dist/models/Presentation.d.ts.map +1 -1
- package/dist/models/Profile.d.ts +2 -0
- package/dist/models/Profile.d.ts.map +1 -1
- package/dist/models/ShareLink.d.ts +46 -0
- package/dist/models/ShareLink.d.ts.map +1 -0
- package/dist/models/ShareViewReceipt.d.ts +22 -0
- package/dist/models/ShareViewReceipt.d.ts.map +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/share-link-constraints.d.ts +3 -0
- package/dist/models/share-link-constraints.d.ts.map +1 -0
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/public-share-link-http.d.ts +4 -0
- package/dist/public-share-link-http.d.ts.map +1 -0
- package/dist/routes/app-store.d.ts +619 -0
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +2 -2
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +148 -5
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +35 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +52 -2
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +153 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/presentations.d.ts +1 -0
- package/dist/routes/presentations.d.ts.map +1 -1
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/public-share-links.d.ts +285 -0
- package/dist/routes/public-share-links.d.ts.map +1 -0
- package/dist/routes/share-links.d.ts +1005 -0
- package/dist/routes/share-links.d.ts.map +1 -0
- package/dist/routes/test.d.ts +5 -0
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/types/app-manifest.d.ts +339 -0
- package/dist/types/app-manifest.d.ts.map +1 -0
- package/dist/types/app-store-listing.d.ts +34 -0
- package/dist/types/app-store-listing.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +31 -4
- package/dist/types/consentflowcontract.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +1 -1
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +49 -192
- package/lambdaApp.ts +203 -0
- package/package.json +25 -17
- package/scripts/export-openapi.ts +20 -0
- package/scripts/offline-schema-environment.ts +18 -0
- package/scripts/seed-demo-persona.ts +425 -0
- package/scripts/verify-python-contracts.ts +72 -0
- package/serverless-local.yml +3 -6
- package/serverless.function-env.cjs +50 -0
- package/serverless.local.yml +0 -1
- package/serverless.yml +44 -55
- package/shareLinkMaintenanceLambda.ts +18 -0
- package/shareLinkMaintenanceLambdaApp.ts +50 -0
- package/src/accesslayer/app-manifest-version/create.ts +57 -0
- package/src/accesslayer/app-manifest-version/read.ts +129 -0
- package/src/accesslayer/app-manifest-version/update.ts +61 -0
- package/src/accesslayer/app-store-listing/read.ts +6 -0
- package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
- package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
- package/src/accesslayer/app-store-listing/update.ts +15 -0
- package/src/accesslayer/boost/relationships/create.ts +8 -2
- package/src/accesslayer/consentflowcontract/create.ts +81 -8
- package/src/accesslayer/consentflowcontract/read.ts +84 -10
- package/src/accesslayer/consentflowcontract/relationships/create.ts +125 -109
- package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
- package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
- package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/update.ts +255 -217
- package/src/accesslayer/credential/create.test.ts +2 -115
- package/src/accesslayer/credential/create.ts +11 -6
- package/src/accesslayer/credential/relationships/create.ts +17 -10
- package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
- package/src/accesslayer/inbox-credential/update.ts +4 -2
- package/src/accesslayer/presentation/read.ts +57 -25
- package/src/accesslayer/presentation/relationships/create.ts +54 -11
- package/src/accesslayer/presentation/relationships/read.ts +21 -6
- package/src/accesslayer/share-link/abandon.ts +120 -0
- package/src/accesslayer/share-link/attachment.ts +360 -0
- package/src/accesslayer/share-link/cleanup.ts +271 -0
- package/src/accesslayer/share-link/errors.ts +32 -0
- package/src/accesslayer/share-link/finalize.ts +313 -0
- package/src/accesslayer/share-link/helpers.ts +450 -0
- package/src/accesslayer/share-link/index.ts +11 -0
- package/src/accesslayer/share-link/list.ts +162 -0
- package/src/accesslayer/share-link/read.ts +107 -0
- package/src/accesslayer/share-link/receipt.ts +548 -0
- package/src/accesslayer/share-link/recovery.ts +565 -0
- package/src/accesslayer/share-link/reserve.ts +763 -0
- package/src/accesslayer/share-link/revoke.ts +232 -0
- package/src/accesslayer/share-link/transaction-execution.ts +72 -0
- package/src/accesslayer/share-link/transaction.ts +126 -0
- package/src/accesslayer/share-link/types.ts +606 -0
- package/src/app.ts +6 -0
- package/src/cache/app-store.caches.ts +35 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/environment.test.ts +17 -12
- package/src/config/environment.ts +15 -0
- package/src/config/serverlessFunctionEnv.test.ts +105 -0
- package/src/config/stageConfig.test.ts +25 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/constants/auth-grant.ts +5 -0
- package/src/docker-entry.ts +81 -0
- package/src/helpers/app-manifest.helpers.ts +185 -0
- package/src/helpers/boost.helpers.ts +4 -31
- package/src/helpers/claim-hooks.helpers.ts +20 -7
- package/src/helpers/consent-referral.helpers.ts +6 -0
- package/src/helpers/consent-scopes.helpers.ts +58 -0
- package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
- package/src/helpers/contract-events.helpers.ts +540 -0
- package/src/helpers/contract-requests.helpers.ts +211 -0
- package/src/helpers/credentialSubject.helpers.test.ts +114 -0
- package/src/helpers/credentialSubject.helpers.ts +40 -0
- package/src/helpers/finalize-inbox.helpers.ts +163 -44
- package/src/helpers/inbox.helpers.ts +58 -70
- package/src/helpers/inline-template.helpers.ts +560 -0
- package/src/helpers/notificationMessages.ts +77 -1
- package/src/helpers/notificationQueue.helpers.ts +23 -5
- package/src/helpers/notifications.helpers.ts +25 -1
- package/src/helpers/presentation-metadata.ts +41 -0
- package/src/helpers/presentation.helpers.ts +27 -20
- package/src/helpers/profile.helpers.ts +13 -0
- package/src/helpers/share-content-client/adapters.ts +55 -0
- package/src/helpers/share-content-client/canonical.ts +25 -0
- package/src/helpers/share-content-client/client.ts +440 -0
- package/src/helpers/share-content-client/config.ts +189 -0
- package/src/helpers/share-content-client/index.ts +27 -0
- package/src/helpers/share-content-client/responses.ts +273 -0
- package/src/helpers/share-content-client/types.ts +253 -0
- package/src/helpers/share-link-attachment-access.ts +87 -0
- package/src/helpers/share-link-attachment.ts +115 -0
- package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
- package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
- package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
- package/src/helpers/share-link-coordinator/coordinator.ts +727 -0
- package/src/helpers/share-link-coordinator/index.ts +8 -0
- package/src/helpers/share-link-coordinator/recovery-runner.ts +443 -0
- package/src/helpers/share-link-coordinator/runtime.ts +88 -0
- package/src/helpers/share-link-coordinator/stat.ts +30 -0
- package/src/helpers/share-link-coordinator/types.ts +353 -0
- package/src/helpers/share-link-lifecycle/index.ts +4 -0
- package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
- package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
- package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
- package/src/helpers/share-link-lifecycle/types.ts +58 -0
- package/src/helpers/share-link-maintenance/budget.ts +63 -0
- package/src/helpers/share-link-maintenance/config.ts +312 -0
- package/src/helpers/share-link-maintenance/index.ts +24 -0
- package/src/helpers/share-link-maintenance/runner.ts +251 -0
- package/src/helpers/share-link-maintenance/runtime.ts +340 -0
- package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
- package/src/helpers/share-link-maintenance/telemetry.ts +73 -0
- package/src/helpers/share-link-maintenance/types.ts +91 -0
- package/src/helpers/share-link-owner/config.ts +48 -0
- package/src/helpers/share-link-owner/diagnostics.ts +89 -0
- package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
- package/src/helpers/share-link-owner-projection.ts +54 -0
- package/src/helpers/share-link-passcode-abuse.ts +36 -0
- package/src/helpers/share-link-passcode.ts +47 -0
- package/src/helpers/share-link-policy/index.ts +7 -0
- package/src/helpers/share-link-policy/production.ts +108 -0
- package/src/helpers/share-link-policy/resolver.ts +113 -0
- package/src/helpers/share-link-policy/types.ts +56 -0
- package/src/helpers/share-link-public/config.ts +41 -0
- package/src/helpers/share-link-view-notification.ts +18 -0
- package/src/helpers/signingAuthority.helpers.ts +23 -14
- package/src/helpers/smartResume.helpers.ts +123 -0
- package/src/models/AppManifestVersion.ts +43 -0
- package/src/models/AppStoreListing.ts +32 -0
- package/src/models/ConsentFlowContract.ts +34 -3
- package/src/models/ConsentFlowTerms.ts +10 -0
- package/src/models/ConsentFlowTransaction.ts +3 -0
- package/src/models/Integration.ts +2 -0
- package/src/models/Presentation.ts +2 -2
- package/src/models/Profile.ts +3 -2
- package/src/models/ShareLink.ts +134 -0
- package/src/models/ShareViewReceipt.ts +69 -0
- package/src/models/index.ts +25 -0
- package/src/models/share-link-constraints.ts +98 -0
- package/src/openapi.ts +44 -8
- package/src/public-share-link-http.ts +49 -0
- package/src/routes/app-store.ts +1056 -38
- package/src/routes/boosts.ts +14 -25
- package/src/routes/contracts.ts +384 -273
- package/src/routes/inbox.ts +4 -2
- package/src/routes/index.ts +142 -44
- package/src/routes/presentations.ts +19 -3
- package/src/routes/profile-manager.ts +29 -2
- package/src/routes/profiles.ts +29 -6
- package/src/routes/public-share-links.ts +983 -0
- package/src/routes/share-links.ts +709 -0
- package/src/routes/workflows.ts +3 -2
- package/src/services/delivery/adapters/postmark.adapter.ts +4 -1
- package/src/types/app-manifest.ts +28 -0
- package/src/types/app-store-listing.ts +13 -0
- package/src/types/consentflowcontract.ts +25 -4
- package/test/app-manifest.helpers.spec.ts +170 -0
- package/test/app-manifest.routes.spec.ts +299 -0
- package/test/app-store-listing-updates.spec.ts +236 -0
- package/test/app-store-listing.spec.ts +65 -3
- package/test/consent-data-boundaries.spec.ts +352 -0
- package/test/consent-recipients.spec.ts +513 -0
- package/test/consent-scopes.helpers.spec.ts +67 -0
- package/test/consentflow.spec.ts +63 -2
- package/test/contract-requests.spec.ts +1588 -0
- package/test/credential-storage.neo4j.spec.ts +122 -0
- package/test/dids.spec.ts +10 -1
- package/test/guardian-gated-routes.spec.ts +49 -0
- package/test/helpers/share-link-fixtures.ts +94 -0
- package/test/inbox-security.e2e.ts +26 -10
- package/test/inbox.spec.ts +184 -8
- package/test/inline-template.helpers.spec.ts +49 -0
- package/test/managed-recipient-inbox.spec.ts +152 -0
- package/test/notificationMessages.spec.ts +13 -0
- package/test/openapi.spec.ts +14 -53
- package/test/presentations.spec.ts +54 -6
- package/test/profiles.spec.ts +97 -1
- package/test/share-content-client.contract.spec.ts +365 -0
- package/test/share-content-client.unit.spec.ts +462 -0
- package/test/share-link-abuse.unit.spec.ts +65 -0
- package/test/share-link-attachment.neo4j.spec.ts +262 -0
- package/test/share-link-attachment.unit.spec.ts +244 -0
- package/test/share-link-coordinator.neo4j.spec.ts +684 -0
- package/test/share-link-coordinator.unit.spec.ts +990 -0
- package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
- package/test/share-link-lifecycle.neo4j.spec.ts +1411 -0
- package/test/share-link-lifecycle.unit.spec.ts +205 -0
- package/test/share-link-list-cursor.unit.spec.ts +86 -0
- package/test/share-link-list.neo4j.spec.ts +324 -0
- package/test/share-link-maintenance.neo4j.spec.ts +355 -0
- package/test/share-link-maintenance.unit.spec.ts +1140 -0
- package/test/share-link-owner-policy.unit.spec.ts +210 -0
- package/test/share-link-owner-route.unit.spec.ts +820 -0
- package/test/share-link-owner-runtime.unit.spec.ts +102 -0
- package/test/share-link-passcode.unit.spec.ts +29 -0
- package/test/share-link-policy.unit.spec.ts +149 -0
- package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
- package/test/share-link-production-policy.unit.spec.ts +126 -0
- package/test/share-link-public-config.unit.spec.ts +64 -0
- package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
- package/test/share-link-public-route.unit.spec.ts +1079 -0
- package/test/share-link-receipt.neo4j.spec.ts +943 -0
- package/test/share-link-recovery.neo4j.spec.ts +935 -0
- package/test/share-link-recovery.unit.spec.ts +726 -0
- package/test/share-link-service-policy.neo4j.spec.ts +403 -0
- package/test/signing-authority-encryption.spec.ts +75 -0
- package/test/smart-resume-consent.spec.ts +233 -0
- package/test/smart-resume-upload.spec.ts +65 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +14 -0
- package/vitest.inbox-e2e.config.ts +2 -0
- package/vitest.share-links-neo4j.config.ts +19 -0
- package/vitest.share-links-unit.config.ts +28 -0
- package/workerLambda.test.ts +100 -0
package/src/routes/app-store.ts
CHANGED
|
@@ -2,10 +2,20 @@ import { TRPCError } from '@trpc/server';
|
|
|
2
2
|
import { v4 as uuid } from 'uuid';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import {
|
|
5
|
+
PendingListingUpdateValidator,
|
|
6
|
+
AppManifestDiffValidator,
|
|
7
|
+
AppManifestValidator,
|
|
8
|
+
AppManifestVersionValidator,
|
|
5
9
|
AppEventValidator,
|
|
6
10
|
LCNNotificationTypeEnumValidator,
|
|
7
11
|
SendNotificationEventValidator,
|
|
8
12
|
} from '@learncard/types';
|
|
13
|
+
import { mergeCapturedManifests } from '@learncard/partner-connect-core';
|
|
14
|
+
import type {
|
|
15
|
+
ConsentRequest,
|
|
16
|
+
InlineCredentialTemplate,
|
|
17
|
+
NormalizedConsentScopes,
|
|
18
|
+
} from '@learncard/partner-connect-core';
|
|
9
19
|
import type { UnsignedVC } from '@learncard/types';
|
|
10
20
|
import { isVC2Format, checkAppInstallEligibility, calculateAgeFromDob } from '@learncard/helpers';
|
|
11
21
|
import type { ProfileType } from 'types/profile';
|
|
@@ -18,18 +28,28 @@ import { addNotificationToQueue } from '@helpers/notifications.helpers';
|
|
|
18
28
|
import { getNotificationMessage } from '@helpers/notificationMessages';
|
|
19
29
|
import { resolveRecipientLocale } from '@helpers/getRecipientLocale.helpers';
|
|
20
30
|
import { PerfTracker } from '@helpers/perf';
|
|
31
|
+
import {
|
|
32
|
+
buildConsentFlowContractFromScopes,
|
|
33
|
+
buildScopedConsentContractName,
|
|
34
|
+
computeConsentScopeHash,
|
|
35
|
+
normalizeConsentScopes,
|
|
36
|
+
} from '@helpers/consent-scopes.helpers';
|
|
37
|
+
import { diffManifests, hashManifest } from '@helpers/app-manifest.helpers';
|
|
21
38
|
import cache from '@cache';
|
|
22
39
|
import {
|
|
23
40
|
getBoostForListingCached,
|
|
41
|
+
getConsentContractForListingByScopeHashCached,
|
|
24
42
|
readAppStoreListingByIdCached,
|
|
25
43
|
getIntegrationForListingCached,
|
|
26
44
|
getOwnerProfileForIntegrationCached,
|
|
27
45
|
getPrimarySigningAuthorityForListingCached,
|
|
28
46
|
getPrimarySigningAuthorityForIntegrationCached,
|
|
47
|
+
invalidateBoostForListingCache,
|
|
48
|
+
invalidateConsentContractForListingByScopeHashCache,
|
|
29
49
|
} from '@cache/app-store.caches';
|
|
30
50
|
import { logCredentialSent } from '@helpers/activity.helpers';
|
|
31
51
|
import { getCredentialUri } from '@helpers/credential.helpers';
|
|
32
|
-
import { resolveUri } from '@helpers/uri.helpers';
|
|
52
|
+
import { constructUri, resolveUri } from '@helpers/uri.helpers';
|
|
33
53
|
import { inflateObject } from '@helpers/objects.helpers';
|
|
34
54
|
import { getAvailableAppSlug } from '@helpers/slug.helpers';
|
|
35
55
|
import {
|
|
@@ -52,13 +72,17 @@ import {
|
|
|
52
72
|
countInstalledAppsForProfile,
|
|
53
73
|
checkIfProfileInstalledApp,
|
|
54
74
|
} from '@accesslayer/app-store-listing/read';
|
|
55
|
-
import {
|
|
75
|
+
import {
|
|
76
|
+
clearPendingListingUpdate,
|
|
77
|
+
updateAppStoreListing,
|
|
78
|
+
} from '@accesslayer/app-store-listing/update';
|
|
56
79
|
import { deleteAppStoreListing } from '@accesslayer/app-store-listing/delete';
|
|
57
80
|
import {
|
|
58
81
|
associateListingWithIntegration,
|
|
59
82
|
associateListingWithSubmitter,
|
|
60
83
|
installAppForProfile,
|
|
61
84
|
associateBoostWithListing,
|
|
85
|
+
associateConsentContractWithListing,
|
|
62
86
|
} from '@accesslayer/app-store-listing/relationships/create';
|
|
63
87
|
import {
|
|
64
88
|
uninstallAppForProfile,
|
|
@@ -91,11 +115,13 @@ import {
|
|
|
91
115
|
PromotionLevel,
|
|
92
116
|
AppStoreListingValidator,
|
|
93
117
|
AgeRating,
|
|
118
|
+
StoredPendingListingUpdateValidator,
|
|
94
119
|
} from 'types/app-store-listing';
|
|
95
120
|
import type {
|
|
96
121
|
AppStoreListingCreateType,
|
|
97
122
|
AppStoreListingType,
|
|
98
123
|
AppStoreListingUpdateType,
|
|
124
|
+
StoredPendingListingUpdate,
|
|
99
125
|
} from 'types/app-store-listing';
|
|
100
126
|
import { getBoostByUri } from '@accesslayer/boost/read';
|
|
101
127
|
import {
|
|
@@ -104,10 +130,32 @@ import {
|
|
|
104
130
|
isDraftBoost,
|
|
105
131
|
appendTemplateEvidenceToCredential,
|
|
106
132
|
} from '@helpers/boost.helpers';
|
|
133
|
+
import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
|
|
107
134
|
import { createBoostForListing } from '@accesslayer/boost/create';
|
|
135
|
+
import { createConsentFlowContract } from '@accesslayer/consentflowcontract/create';
|
|
136
|
+
import { createAppManifestVersion } from '@accesslayer/app-manifest-version/create';
|
|
137
|
+
import {
|
|
138
|
+
getActiveManifestVersionForIntegration,
|
|
139
|
+
getLatestManifestVersionForIntegration,
|
|
140
|
+
getManifestVersionForIntegration,
|
|
141
|
+
getManifestVersionsForIntegration,
|
|
142
|
+
} from '@accesslayer/app-manifest-version/read';
|
|
143
|
+
import {
|
|
144
|
+
associateListingWithManifestVersion,
|
|
145
|
+
markManifestVersionsSuperseded,
|
|
146
|
+
updateAppManifestVersion,
|
|
147
|
+
} from '@accesslayer/app-manifest-version/update';
|
|
108
148
|
import { setBoostAsParent } from '@accesslayer/boost/relationships/create';
|
|
109
149
|
import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
|
|
110
150
|
import type { IssuedCredential } from '../types/credential';
|
|
151
|
+
import {
|
|
152
|
+
computeInlineTemplateHash,
|
|
153
|
+
ensureManagedSigningAuthorityForListing,
|
|
154
|
+
getInlineTemplateVersion,
|
|
155
|
+
readInlineTemplateBoostMeta,
|
|
156
|
+
upsertInlineTemplateBoostForListing,
|
|
157
|
+
validateInlineTemplateDataOrThrow,
|
|
158
|
+
} from '@helpers/inline-template.helpers';
|
|
111
159
|
import {
|
|
112
160
|
renderBoostTemplate,
|
|
113
161
|
parseRenderedTemplate,
|
|
@@ -178,8 +226,14 @@ const isAllowedImageUrl = (url: string): boolean => {
|
|
|
178
226
|
const isValidIframeUrl = (url: string): boolean => {
|
|
179
227
|
try {
|
|
180
228
|
const parsed = new URL(url);
|
|
181
|
-
// Only allow https URLs for iframes
|
|
182
|
-
if (parsed.protocol !== 'https:')
|
|
229
|
+
// Only allow https URLs for iframes (except for local preview of draft listings)
|
|
230
|
+
if (parsed.protocol !== 'https:') {
|
|
231
|
+
const isLocalhost =
|
|
232
|
+
parsed.hostname === 'localhost' ||
|
|
233
|
+
parsed.hostname === '127.0.0.1' ||
|
|
234
|
+
parsed.hostname === '[::1]';
|
|
235
|
+
if (!isLocalhost) return false;
|
|
236
|
+
}
|
|
183
237
|
// Block javascript:, data:, and vbscript: schemes (already handled by URL parsing, but be explicit)
|
|
184
238
|
const lowerUrl = url.toLowerCase();
|
|
185
239
|
if (
|
|
@@ -291,7 +345,14 @@ const iframeUrlRefinement = (
|
|
|
291
345
|
if (config.iframeUrl && !isValidIframeUrl(config.iframeUrl)) {
|
|
292
346
|
ctx.addIssue({
|
|
293
347
|
code: z.ZodIssueCode.custom,
|
|
294
|
-
message: 'Iframe URL must be a valid HTTPS URL',
|
|
348
|
+
message: 'Iframe URL must be a valid HTTPS URL (or localhost for testing)',
|
|
349
|
+
path: ['launch_config_json'],
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (config.url && !isValidIframeUrl(config.url)) {
|
|
353
|
+
ctx.addIssue({
|
|
354
|
+
code: z.ZodIssueCode.custom,
|
|
355
|
+
message: 'URL must be a valid HTTPS URL (or localhost for testing)',
|
|
295
356
|
path: ['launch_config_json'],
|
|
296
357
|
});
|
|
297
358
|
}
|
|
@@ -301,12 +362,16 @@ const iframeUrlRefinement = (
|
|
|
301
362
|
}
|
|
302
363
|
};
|
|
303
364
|
|
|
365
|
+
type PendingStorageField =
|
|
366
|
+
'pending_update_json' | 'pending_update_status' | 'pending_update_submitted_at';
|
|
367
|
+
|
|
304
368
|
type AppStoreListingResponse<T extends AppStoreListingType> = Omit<
|
|
305
369
|
T,
|
|
306
|
-
'highlights_json' | 'screenshots_json'
|
|
370
|
+
'highlights_json' | 'screenshots_json' | PendingStorageField
|
|
307
371
|
> & {
|
|
308
372
|
highlights?: string[];
|
|
309
373
|
screenshots?: string[];
|
|
374
|
+
pending_update?: z.infer<typeof PendingListingUpdateValidator>;
|
|
310
375
|
};
|
|
311
376
|
|
|
312
377
|
type ListingStorageInput<T extends Record<string, unknown>> = Omit<
|
|
@@ -320,18 +385,72 @@ type ListingStorageInput<T extends Record<string, unknown>> = Omit<
|
|
|
320
385
|
// Helper to strip sensitive fields (contact_email) from listings for public responses
|
|
321
386
|
const stripSensitiveFields = <T extends Record<string, unknown>>(
|
|
322
387
|
listing: T
|
|
323
|
-
): Omit<T, 'contact_email'> => {
|
|
324
|
-
const { contact_email, ...rest } = listing;
|
|
325
|
-
return rest as Omit<T, 'contact_email'>;
|
|
388
|
+
): Omit<T, 'contact_email' | 'pending_update'> => {
|
|
389
|
+
const { contact_email, pending_update, ...rest } = listing;
|
|
390
|
+
return rest as Omit<T, 'contact_email' | 'pending_update'>;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const parseJsonArray = (value: unknown): string[] | undefined => {
|
|
394
|
+
if (typeof value !== 'string' || !value) return undefined;
|
|
395
|
+
try {
|
|
396
|
+
const parsed: unknown = JSON.parse(value);
|
|
397
|
+
return Array.isArray(parsed) ? parsed.filter(item => typeof item === 'string') : [];
|
|
398
|
+
} catch {
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const readPendingUpdate = (listing: AppStoreListingType): StoredPendingListingUpdate | null => {
|
|
404
|
+
if (!listing.pending_update_json) return null;
|
|
405
|
+
try {
|
|
406
|
+
const parsed = StoredPendingListingUpdateValidator.safeParse(
|
|
407
|
+
JSON.parse(listing.pending_update_json)
|
|
408
|
+
);
|
|
409
|
+
return parsed.success ? parsed.data : null;
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const toPendingUpdateResponse = (
|
|
416
|
+
listing: AppStoreListingType
|
|
417
|
+
): z.infer<typeof PendingListingUpdateValidator> | undefined => {
|
|
418
|
+
const pending = readPendingUpdate(listing);
|
|
419
|
+
if (!pending || !listing.pending_update_status) return undefined;
|
|
420
|
+
|
|
421
|
+
const { highlights_json, screenshots_json, ...changes } = pending.changes;
|
|
422
|
+
const highlights = parseJsonArray(highlights_json);
|
|
423
|
+
const screenshots = parseJsonArray(screenshots_json);
|
|
424
|
+
|
|
425
|
+
return PendingListingUpdateValidator.parse({
|
|
426
|
+
status: listing.pending_update_status,
|
|
427
|
+
submitted_at: listing.pending_update_submitted_at,
|
|
428
|
+
changes: {
|
|
429
|
+
...changes,
|
|
430
|
+
...(highlights ? { highlights } : {}),
|
|
431
|
+
...(screenshots ? { screenshots } : {}),
|
|
432
|
+
},
|
|
433
|
+
manifest_version: pending.manifestVersion,
|
|
434
|
+
});
|
|
326
435
|
};
|
|
327
436
|
|
|
328
437
|
// Helper to transform listing for API response (JSON strings -> arrays)
|
|
329
438
|
const transformListingForResponse = <T extends AppStoreListingType & Record<string, unknown>>(
|
|
330
439
|
listing: T
|
|
331
440
|
): AppStoreListingResponse<T> => {
|
|
332
|
-
const {
|
|
441
|
+
const {
|
|
442
|
+
highlights_json,
|
|
443
|
+
screenshots_json,
|
|
444
|
+
pending_update_json,
|
|
445
|
+
pending_update_status,
|
|
446
|
+
pending_update_submitted_at,
|
|
447
|
+
...rest
|
|
448
|
+
} = listing;
|
|
333
449
|
const result = { ...rest } as AppStoreListingResponse<T>;
|
|
334
450
|
|
|
451
|
+
const pendingUpdate = toPendingUpdateResponse(listing);
|
|
452
|
+
if (pendingUpdate) result.pending_update = pendingUpdate;
|
|
453
|
+
|
|
335
454
|
if (highlights_json) {
|
|
336
455
|
try {
|
|
337
456
|
result.highlights = JSON.parse(highlights_json);
|
|
@@ -441,9 +560,13 @@ const AppStoreListingResponseValidator = AppStoreListingValidator.extend({
|
|
|
441
560
|
highlights: z.array(z.string()).optional(),
|
|
442
561
|
screenshots: z.array(z.string()).optional(),
|
|
443
562
|
submitter: AppStoreListingSubmitterValidator.optional(),
|
|
563
|
+
pending_update: PendingListingUpdateValidator.optional(),
|
|
444
564
|
}).omit({
|
|
445
565
|
highlights_json: true,
|
|
446
566
|
screenshots_json: true,
|
|
567
|
+
pending_update_json: true,
|
|
568
|
+
pending_update_status: true,
|
|
569
|
+
pending_update_submitted_at: true,
|
|
447
570
|
});
|
|
448
571
|
|
|
449
572
|
const PaginatedAppStoreListingsValidator = z.object({
|
|
@@ -479,6 +602,45 @@ const PaginatedAppCredentialsValidator = z.object({
|
|
|
479
602
|
totalCount: z.number(),
|
|
480
603
|
});
|
|
481
604
|
|
|
605
|
+
const AppManifestVersionSummaryValidator = AppManifestVersionValidator.omit({ manifest: true });
|
|
606
|
+
|
|
607
|
+
const PaginatedAppManifestVersionsValidator = z.object({
|
|
608
|
+
hasMore: z.boolean(),
|
|
609
|
+
cursor: z.string().optional(),
|
|
610
|
+
records: z.array(AppManifestVersionSummaryValidator),
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const SubmitAppManifestResponseValidator = z.object({
|
|
614
|
+
version: z.number().int().min(1),
|
|
615
|
+
manifestHash: z.string(),
|
|
616
|
+
diff: AppManifestDiffValidator.nullable(),
|
|
617
|
+
noop: z.boolean(),
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const ApplyManifestVersionResponseValidator = z.object({
|
|
621
|
+
applied: z.boolean(),
|
|
622
|
+
pendingReview: z.boolean().optional(),
|
|
623
|
+
version: z.number().int().min(1),
|
|
624
|
+
reconciled: z.object({
|
|
625
|
+
templatesUpserted: z.number().int().min(0),
|
|
626
|
+
templatesSkipped: z.number().int().min(0),
|
|
627
|
+
contractsUpserted: z.number().int().min(0),
|
|
628
|
+
contractsSkipped: z.number().int().min(0),
|
|
629
|
+
signingAuthorityEnsured: z.boolean(),
|
|
630
|
+
}),
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const toManifestVersionSummary = (
|
|
634
|
+
manifestVersion: z.infer<typeof AppManifestVersionValidator>
|
|
635
|
+
): z.infer<typeof AppManifestVersionSummaryValidator> => ({
|
|
636
|
+
id: manifestVersion.id,
|
|
637
|
+
version: manifestVersion.version,
|
|
638
|
+
manifestHash: manifestVersion.manifestHash,
|
|
639
|
+
status: manifestVersion.status,
|
|
640
|
+
createdAt: manifestVersion.createdAt,
|
|
641
|
+
activatedAt: manifestVersion.activatedAt,
|
|
642
|
+
});
|
|
643
|
+
|
|
482
644
|
// Helper to verify integration ownership
|
|
483
645
|
const verifyIntegrationOwnership = async (integrationId: string, profileId: string) => {
|
|
484
646
|
const associated = await isIntegrationAssociatedWithProfile(integrationId, profileId);
|
|
@@ -557,12 +719,17 @@ export const handleSendCredentialEvent = async (
|
|
|
557
719
|
): Promise<Record<string, unknown>> => {
|
|
558
720
|
const perf = perfTracker ?? new PerfTracker('handleSendCredentialEvent');
|
|
559
721
|
|
|
560
|
-
const templateAlias =
|
|
722
|
+
const templateAlias =
|
|
723
|
+
(event.templateAlias as string | undefined) ?? (event.alias as string | undefined);
|
|
724
|
+
const inlineTemplate = event.template as
|
|
725
|
+
import('@learncard/partner-connect-core').InlineCredentialTemplate | undefined;
|
|
561
726
|
const templateData = event.templateData as Record<string, unknown> | undefined;
|
|
562
727
|
const preventDuplicateClaim = Boolean(event.preventDuplicateClaim);
|
|
728
|
+
const isInlineTemplateEvent = Boolean(inlineTemplate);
|
|
729
|
+
let templateVersion: number | undefined = undefined;
|
|
563
730
|
|
|
564
731
|
if (!templateAlias) {
|
|
565
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: 'templateAlias required' });
|
|
732
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'templateAlias or alias required' });
|
|
566
733
|
}
|
|
567
734
|
|
|
568
735
|
// LC-1644 Phase 1b: Fire the four input-only lookups in parallel. All depend only on
|
|
@@ -571,17 +738,16 @@ export const handleSendCredentialEvent = async (
|
|
|
571
738
|
// parallel batching + 30s LRU cache on the first three below compounds to ~50-150ms
|
|
572
739
|
// saved on warm lambda invocations. targetProfile is keyed by the requester's
|
|
573
740
|
// profileId (varies per user) so it's not cached.
|
|
574
|
-
const [
|
|
575
|
-
|
|
741
|
+
const [cachedBoostResult, listing, integration, targetProfile] = await Promise.all([
|
|
742
|
+
isInlineTemplateEvent
|
|
743
|
+
? Promise.resolve(null)
|
|
744
|
+
: getBoostForListingCached(listingId, templateAlias, ctx.domain),
|
|
576
745
|
readAppStoreListingByIdCached(listingId),
|
|
577
746
|
getIntegrationForListingCached(listingId),
|
|
578
747
|
getProfilesByProfileIds([profile.profileId]),
|
|
579
748
|
]);
|
|
580
749
|
perf.mark('parallelReads');
|
|
581
750
|
|
|
582
|
-
if (!boostResult) {
|
|
583
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'Boost not found for this app' });
|
|
584
|
-
}
|
|
585
751
|
if (!listing) {
|
|
586
752
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'App Store Listing not found' });
|
|
587
753
|
}
|
|
@@ -593,7 +759,30 @@ export const handleSendCredentialEvent = async (
|
|
|
593
759
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found' });
|
|
594
760
|
}
|
|
595
761
|
|
|
762
|
+
const boostResult = isInlineTemplateEvent
|
|
763
|
+
? await upsertInlineTemplateBoostForListing({
|
|
764
|
+
listing,
|
|
765
|
+
integration,
|
|
766
|
+
templateAlias,
|
|
767
|
+
template: inlineTemplate!,
|
|
768
|
+
domain: ctx.domain,
|
|
769
|
+
})
|
|
770
|
+
: cachedBoostResult;
|
|
771
|
+
|
|
772
|
+
if (!boostResult) {
|
|
773
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Boost not found for this app' });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (isInlineTemplateEvent) {
|
|
777
|
+
const inlineBoostResult = boostResult as Awaited<
|
|
778
|
+
ReturnType<typeof upsertInlineTemplateBoostForListing>
|
|
779
|
+
>;
|
|
780
|
+
validateInlineTemplateDataOrThrow(inlineBoostResult.manifest, templateData);
|
|
781
|
+
templateVersion = inlineBoostResult.templateVersion;
|
|
782
|
+
}
|
|
783
|
+
|
|
596
784
|
const { boost, boostUri } = boostResult;
|
|
785
|
+
templateVersion ??= getInlineTemplateVersion(boost);
|
|
597
786
|
|
|
598
787
|
// NOTE: This is best-effort duplicate prevention, not a guarantee.
|
|
599
788
|
// There is a race condition window between checking and creating the credential
|
|
@@ -617,6 +806,7 @@ export const handleSendCredentialEvent = async (
|
|
|
617
806
|
receivedDate: existingCredential.receivedDate ?? existingCredential.sentDate,
|
|
618
807
|
status: existingCredential.status,
|
|
619
808
|
boostUri,
|
|
809
|
+
...(templateVersion !== undefined ? { templateVersion } : {}),
|
|
620
810
|
// credential not included — frontend skips modal when alreadyClaimed=true
|
|
621
811
|
};
|
|
622
812
|
}
|
|
@@ -651,17 +841,19 @@ export const handleSendCredentialEvent = async (
|
|
|
651
841
|
listingSa || integrationSa
|
|
652
842
|
? undefined
|
|
653
843
|
: await getPrimarySigningAuthorityForUser(integrationOwner);
|
|
654
|
-
const sa =
|
|
844
|
+
const sa =
|
|
845
|
+
listingSa ??
|
|
846
|
+
integrationSa ??
|
|
847
|
+
ownerSa ??
|
|
848
|
+
(await ensureManagedSigningAuthorityForListing(
|
|
849
|
+
listing,
|
|
850
|
+
integration,
|
|
851
|
+
integrationOwner,
|
|
852
|
+
ctx.domain
|
|
853
|
+
));
|
|
655
854
|
|
|
656
855
|
perf.mark('saResolve');
|
|
657
856
|
|
|
658
|
-
if (!sa) {
|
|
659
|
-
throw new TRPCError({
|
|
660
|
-
code: 'NOT_FOUND',
|
|
661
|
-
message: 'No signing authority configured for this app',
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
|
|
665
857
|
// Build unsigned credential from boost template
|
|
666
858
|
let unsignedVc: UnsignedVC;
|
|
667
859
|
|
|
@@ -692,17 +884,7 @@ export const handleSendCredentialEvent = async (
|
|
|
692
884
|
|
|
693
885
|
const targetDid = getDidWeb(ctx.domain, target.profileId);
|
|
694
886
|
|
|
695
|
-
|
|
696
|
-
unsignedVc.credentialSubject = unsignedVc.credentialSubject.map(subject => ({
|
|
697
|
-
...subject,
|
|
698
|
-
id: targetDid,
|
|
699
|
-
}));
|
|
700
|
-
} else {
|
|
701
|
-
unsignedVc.credentialSubject = {
|
|
702
|
-
...unsignedVc.credentialSubject,
|
|
703
|
-
id: targetDid,
|
|
704
|
-
};
|
|
705
|
-
}
|
|
887
|
+
setCredentialSubjectIds(unsignedVc, targetDid);
|
|
706
888
|
|
|
707
889
|
if (unsignedVc?.type?.includes('BoostCredential')) {
|
|
708
890
|
unsignedVc.boostId = boostUri;
|
|
@@ -835,6 +1017,7 @@ export const handleSendCredentialEvent = async (
|
|
|
835
1017
|
return {
|
|
836
1018
|
credentialUri,
|
|
837
1019
|
boostUri,
|
|
1020
|
+
...(templateVersion !== undefined ? { templateVersion } : {}),
|
|
838
1021
|
// LC-1644: pre-resolved (enriched) credential so the frontend modal
|
|
839
1022
|
// can render the proper title + issuer info without shimmer or
|
|
840
1023
|
// background re-fetch. See enrichment block above for details.
|
|
@@ -842,6 +1025,330 @@ export const handleSendCredentialEvent = async (
|
|
|
842
1025
|
};
|
|
843
1026
|
};
|
|
844
1027
|
|
|
1028
|
+
const normalizeConsentRequestOrThrow = (scopes: ConsentRequest): NormalizedConsentScopes => {
|
|
1029
|
+
try {
|
|
1030
|
+
return normalizeConsentScopes(scopes);
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
const message = error instanceof Error ? error.message : 'Invalid consent scopes';
|
|
1033
|
+
|
|
1034
|
+
throw new TRPCError({
|
|
1035
|
+
code: 'BAD_REQUEST',
|
|
1036
|
+
message: `CONSENT_SCOPES_INVALID: ${message}`,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
const handleUpsertConsentContractEvent = async (
|
|
1042
|
+
ctx: { domain: string },
|
|
1043
|
+
listing: AppStoreListingType,
|
|
1044
|
+
listingId: string,
|
|
1045
|
+
event: Record<string, unknown>
|
|
1046
|
+
): Promise<{ contractUri: string; created: boolean }> => {
|
|
1047
|
+
const rawScopes = event.scopes as ConsentRequest;
|
|
1048
|
+
const normalizedScopes = normalizeConsentRequestOrThrow(rawScopes);
|
|
1049
|
+
return upsertConsentContractForListingScopes({
|
|
1050
|
+
ctx,
|
|
1051
|
+
listing,
|
|
1052
|
+
listingId,
|
|
1053
|
+
normalizedScopes,
|
|
1054
|
+
reason:
|
|
1055
|
+
typeof rawScopes.reason === 'string' && rawScopes.reason.trim().length > 0
|
|
1056
|
+
? rawScopes.reason
|
|
1057
|
+
: undefined,
|
|
1058
|
+
});
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
const upsertConsentContractForListingScopes = async ({
|
|
1062
|
+
ctx,
|
|
1063
|
+
listing,
|
|
1064
|
+
listingId,
|
|
1065
|
+
normalizedScopes,
|
|
1066
|
+
reason,
|
|
1067
|
+
}: {
|
|
1068
|
+
ctx: { domain: string };
|
|
1069
|
+
listing: AppStoreListingType;
|
|
1070
|
+
listingId: string;
|
|
1071
|
+
normalizedScopes: NormalizedConsentScopes;
|
|
1072
|
+
reason?: string;
|
|
1073
|
+
}): Promise<{ contractUri: string; created: boolean; scopeHash: string }> => {
|
|
1074
|
+
const scopeHash = computeConsentScopeHash(normalizedScopes);
|
|
1075
|
+
const existingContract = await getConsentContractForListingByScopeHashCached(
|
|
1076
|
+
listingId,
|
|
1077
|
+
scopeHash
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
if (existingContract) {
|
|
1081
|
+
return {
|
|
1082
|
+
contractUri: constructUri('contract', existingContract.id, ctx.domain),
|
|
1083
|
+
created: false,
|
|
1084
|
+
scopeHash,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const integration = await getIntegrationForListingCached(listingId);
|
|
1089
|
+
|
|
1090
|
+
if (!integration) {
|
|
1091
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Integration not found' });
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const ownerProfile = await getOwnerProfileForIntegrationCached(integration.id);
|
|
1095
|
+
|
|
1096
|
+
if (!ownerProfile) {
|
|
1097
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Integration owner not found' });
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const createdContract = await createConsentFlowContract({
|
|
1101
|
+
contract: buildConsentFlowContractFromScopes(normalizedScopes),
|
|
1102
|
+
ownerProfileId: ownerProfile.profileId,
|
|
1103
|
+
name: buildScopedConsentContractName(listing.display_name),
|
|
1104
|
+
description: listing.tagline,
|
|
1105
|
+
reasonForAccessing: reason,
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
await associateConsentContractWithListing(listingId, createdContract.id, scopeHash);
|
|
1109
|
+
invalidateConsentContractForListingByScopeHashCache(listingId, scopeHash);
|
|
1110
|
+
|
|
1111
|
+
return {
|
|
1112
|
+
contractUri: constructUri('contract', createdContract.id, ctx.domain),
|
|
1113
|
+
created: true,
|
|
1114
|
+
scopeHash,
|
|
1115
|
+
};
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
const resolveManifestApplyListing = async ({
|
|
1119
|
+
integrationId,
|
|
1120
|
+
listingId,
|
|
1121
|
+
profileId,
|
|
1122
|
+
}: {
|
|
1123
|
+
integrationId: string;
|
|
1124
|
+
listingId: string | undefined;
|
|
1125
|
+
profileId: string;
|
|
1126
|
+
}): Promise<AppStoreListingType> => {
|
|
1127
|
+
if (listingId) {
|
|
1128
|
+
const { listing, integration } = await verifyListingOwnership(listingId, profileId);
|
|
1129
|
+
|
|
1130
|
+
if (integration.id !== integrationId) {
|
|
1131
|
+
throw new TRPCError({
|
|
1132
|
+
code: 'BAD_REQUEST',
|
|
1133
|
+
message: 'Listing does not belong to the provided Integration',
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return listing;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const listings = await getListingsForIntegration(integrationId, { limit: 2 });
|
|
1141
|
+
|
|
1142
|
+
if (listings.length === 0) {
|
|
1143
|
+
throw new TRPCError({
|
|
1144
|
+
code: 'NOT_FOUND',
|
|
1145
|
+
message: 'No App Store Listing found for this Integration',
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (listings.length > 1) {
|
|
1150
|
+
throw new TRPCError({
|
|
1151
|
+
code: 'BAD_REQUEST',
|
|
1152
|
+
message: 'listingId is required when an Integration has multiple listings',
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
return listings[0]!;
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
type ManifestVersionRecord = NonNullable<
|
|
1160
|
+
Awaited<ReturnType<typeof getManifestVersionForIntegration>>
|
|
1161
|
+
>;
|
|
1162
|
+
type IntegrationRecord = Awaited<ReturnType<typeof verifyIntegrationOwnership>>;
|
|
1163
|
+
type OwnerProfileRecord = NonNullable<Awaited<ReturnType<typeof getOwnerProfileForIntegration>>>;
|
|
1164
|
+
|
|
1165
|
+
const reconcileManifestVersionForListing = async ({
|
|
1166
|
+
ctx,
|
|
1167
|
+
integration,
|
|
1168
|
+
integrationOwner,
|
|
1169
|
+
listing,
|
|
1170
|
+
manifestVersion,
|
|
1171
|
+
}: {
|
|
1172
|
+
ctx: { domain: string };
|
|
1173
|
+
integration: IntegrationRecord;
|
|
1174
|
+
integrationOwner: OwnerProfileRecord;
|
|
1175
|
+
listing: AppStoreListingType;
|
|
1176
|
+
manifestVersion: ManifestVersionRecord;
|
|
1177
|
+
}) => {
|
|
1178
|
+
let templatesUpserted = 0;
|
|
1179
|
+
let templatesSkipped = 0;
|
|
1180
|
+
let contractsUpserted = 0;
|
|
1181
|
+
let contractsSkipped = 0;
|
|
1182
|
+
|
|
1183
|
+
for (const templateRecord of manifestVersion.manifest.templates) {
|
|
1184
|
+
const inlineTemplate = templateRecord.template as InlineCredentialTemplate;
|
|
1185
|
+
const existingBoost = await getBoostForListingByTemplateAlias(
|
|
1186
|
+
listing.listing_id,
|
|
1187
|
+
templateRecord.alias,
|
|
1188
|
+
ctx.domain
|
|
1189
|
+
);
|
|
1190
|
+
const desiredTemplateHash = computeInlineTemplateHash(inlineTemplate).contentHash;
|
|
1191
|
+
|
|
1192
|
+
const existingBoostMeta = existingBoost
|
|
1193
|
+
? await readInlineTemplateBoostMeta(existingBoost.boost)
|
|
1194
|
+
: undefined;
|
|
1195
|
+
|
|
1196
|
+
if (
|
|
1197
|
+
existingBoostMeta?.inlineTemplateHash === desiredTemplateHash &&
|
|
1198
|
+
existingBoostMeta.inlineTemplateVersion === templateRecord.version
|
|
1199
|
+
) {
|
|
1200
|
+
templatesSkipped += 1;
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
await upsertInlineTemplateBoostForListing({
|
|
1205
|
+
listing,
|
|
1206
|
+
integration,
|
|
1207
|
+
templateAlias: templateRecord.alias,
|
|
1208
|
+
template: inlineTemplate,
|
|
1209
|
+
domain: ctx.domain,
|
|
1210
|
+
desiredVersion: templateRecord.version,
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
templatesUpserted += 1;
|
|
1214
|
+
|
|
1215
|
+
invalidateBoostForListingCache(listing.listing_id, templateRecord.alias, ctx.domain);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
for (const consentRecord of manifestVersion.manifest.consentRequests) {
|
|
1219
|
+
const upsertedConsent = await upsertConsentContractForListingScopes({
|
|
1220
|
+
ctx,
|
|
1221
|
+
listing,
|
|
1222
|
+
listingId: listing.listing_id,
|
|
1223
|
+
normalizedScopes: consentRecord.scopes,
|
|
1224
|
+
reason: consentRecord.reason,
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
if (upsertedConsent.created) {
|
|
1228
|
+
contractsUpserted += 1;
|
|
1229
|
+
} else {
|
|
1230
|
+
contractsSkipped += 1;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
invalidateConsentContractForListingByScopeHashCache(
|
|
1234
|
+
listing.listing_id,
|
|
1235
|
+
upsertedConsent.scopeHash
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
await markManifestVersionsSuperseded(integration.id, manifestVersion.id);
|
|
1240
|
+
|
|
1241
|
+
if (manifestVersion.status !== 'active' || !manifestVersion.activatedAt) {
|
|
1242
|
+
await updateAppManifestVersion(manifestVersion.id, {
|
|
1243
|
+
status: 'active',
|
|
1244
|
+
activatedAt: manifestVersion.activatedAt ?? new Date().toISOString(),
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
await associateListingWithManifestVersion(listing.listing_id, manifestVersion.id);
|
|
1249
|
+
|
|
1250
|
+
await ensureManagedSigningAuthorityForListing(
|
|
1251
|
+
listing,
|
|
1252
|
+
integration,
|
|
1253
|
+
integrationOwner,
|
|
1254
|
+
ctx.domain
|
|
1255
|
+
);
|
|
1256
|
+
|
|
1257
|
+
return {
|
|
1258
|
+
templatesUpserted,
|
|
1259
|
+
templatesSkipped,
|
|
1260
|
+
contractsUpserted,
|
|
1261
|
+
contractsSkipped,
|
|
1262
|
+
signingAuthorityEnsured: true,
|
|
1263
|
+
};
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
const PENDING_UPDATE_IN_REVIEW_MESSAGE =
|
|
1267
|
+
'An update for this app is already in review. Withdraw it to make more changes.';
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Records changes to a LISTED app as a pending update instead of applying them, so
|
|
1271
|
+
* nothing about a live app changes until an admin approves it.
|
|
1272
|
+
*/
|
|
1273
|
+
const holdChangesForReview = async (
|
|
1274
|
+
listing: AppStoreListingType,
|
|
1275
|
+
{
|
|
1276
|
+
changes = {},
|
|
1277
|
+
manifestVersion,
|
|
1278
|
+
}: { changes?: Record<string, unknown>; manifestVersion?: number }
|
|
1279
|
+
): Promise<boolean> => {
|
|
1280
|
+
if (listing.pending_update_status === 'PENDING_REVIEW') {
|
|
1281
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: PENDING_UPDATE_IN_REVIEW_MESSAGE });
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const existing = readPendingUpdate(listing);
|
|
1285
|
+
const next: StoredPendingListingUpdate = {
|
|
1286
|
+
changes: { ...existing?.changes, ...changes },
|
|
1287
|
+
manifestVersion: manifestVersion ?? existing?.manifestVersion,
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
return updateAppStoreListing(listing, {
|
|
1291
|
+
pending_update_json: JSON.stringify(next),
|
|
1292
|
+
pending_update_status: 'DRAFT',
|
|
1293
|
+
});
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
const assertLaunchConfigSubmittable = (launchType: string, launchConfigJson: string): void => {
|
|
1297
|
+
if (launchType !== 'EMBEDDED_IFRAME' || !launchConfigJson) return;
|
|
1298
|
+
|
|
1299
|
+
try {
|
|
1300
|
+
const config = JSON.parse(launchConfigJson);
|
|
1301
|
+
const urlToCheck = config.url || config.iframeUrl;
|
|
1302
|
+
if (!urlToCheck) return;
|
|
1303
|
+
|
|
1304
|
+
const parsed = new URL(urlToCheck);
|
|
1305
|
+
const isLocalhost =
|
|
1306
|
+
parsed.hostname === 'localhost' ||
|
|
1307
|
+
parsed.hostname === '127.0.0.1' ||
|
|
1308
|
+
parsed.hostname === '[::1]';
|
|
1309
|
+
if (isLocalhost || parsed.protocol !== 'https:') {
|
|
1310
|
+
throw new TRPCError({
|
|
1311
|
+
code: 'BAD_REQUEST',
|
|
1312
|
+
message:
|
|
1313
|
+
'Cannot submit an app for review with a localhost or non-HTTPS URL. Please update the launch configuration with your production URL.',
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
} catch (e) {
|
|
1317
|
+
if (e instanceof TRPCError) throw e;
|
|
1318
|
+
// Unparseable config is rejected by input validation.
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
const notifyAppStoreAdmins = async (
|
|
1323
|
+
from: Parameters<typeof addNotificationToQueue>[0]['from'],
|
|
1324
|
+
type: 'APP_LISTING_SUBMITTED' | 'APP_LISTING_WITHDRAWN',
|
|
1325
|
+
listing: AppStoreListingType,
|
|
1326
|
+
metadata: Record<string, unknown> = {}
|
|
1327
|
+
): Promise<void> => {
|
|
1328
|
+
if (APP_STORE_ADMIN_PROFILE_IDS.length === 0) return;
|
|
1329
|
+
|
|
1330
|
+
const adminProfiles = await getProfilesByProfileIds(APP_STORE_ADMIN_PROFILE_IDS);
|
|
1331
|
+
for (const adminProfile of adminProfiles) {
|
|
1332
|
+
await addNotificationToQueue({
|
|
1333
|
+
type,
|
|
1334
|
+
to: adminProfile,
|
|
1335
|
+
from,
|
|
1336
|
+
message: getNotificationMessage(
|
|
1337
|
+
type === 'APP_LISTING_SUBMITTED' ? 'appListingSubmitted' : 'appListingWithdrawn',
|
|
1338
|
+
resolveRecipientLocale(adminProfile),
|
|
1339
|
+
{ displayName: listing.display_name }
|
|
1340
|
+
),
|
|
1341
|
+
data: {
|
|
1342
|
+
metadata: {
|
|
1343
|
+
listingId: listing.listing_id,
|
|
1344
|
+
listingName: listing.display_name,
|
|
1345
|
+
...metadata,
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
|
|
845
1352
|
const handleCheckCredentialEvent = async (
|
|
846
1353
|
ctx: { domain: string },
|
|
847
1354
|
profile: { profileId: string },
|
|
@@ -1839,6 +2346,274 @@ export const appStoreRouter = t.router({
|
|
|
1839
2346
|
return countListingsForIntegration(input.integrationId);
|
|
1840
2347
|
}),
|
|
1841
2348
|
|
|
2349
|
+
submitAppManifest: profileRoute
|
|
2350
|
+
.meta({
|
|
2351
|
+
openapi: {
|
|
2352
|
+
protect: true,
|
|
2353
|
+
method: 'POST',
|
|
2354
|
+
path: '/app-store/integration/{integrationId}/manifest/submit',
|
|
2355
|
+
tags: ['App Store'],
|
|
2356
|
+
summary: 'Submit App Manifest',
|
|
2357
|
+
description:
|
|
2358
|
+
'Create a versioned app manifest draft for an integration, idempotently by manifest hash.',
|
|
2359
|
+
},
|
|
2360
|
+
requiredScope: 'app-store:write',
|
|
2361
|
+
})
|
|
2362
|
+
.input(
|
|
2363
|
+
z.object({
|
|
2364
|
+
integrationId: z.string(),
|
|
2365
|
+
manifest: AppManifestValidator,
|
|
2366
|
+
})
|
|
2367
|
+
)
|
|
2368
|
+
.output(SubmitAppManifestResponseValidator)
|
|
2369
|
+
.mutation(async ({ input, ctx }) => {
|
|
2370
|
+
await verifyIntegrationOwnership(input.integrationId, ctx.user.profile.profileId);
|
|
2371
|
+
|
|
2372
|
+
const latestManifestVersion = await getLatestManifestVersionForIntegration(
|
|
2373
|
+
input.integrationId
|
|
2374
|
+
);
|
|
2375
|
+
const activeManifestVersion = await getActiveManifestVersionForIntegration(
|
|
2376
|
+
input.integrationId
|
|
2377
|
+
);
|
|
2378
|
+
// Each capture is one run of the app; merging keeps what earlier runs saw
|
|
2379
|
+
// so an unexercised feature never shows up as removed.
|
|
2380
|
+
const manifest = latestManifestVersion
|
|
2381
|
+
? mergeCapturedManifests(latestManifestVersion.manifest, input.manifest)
|
|
2382
|
+
: input.manifest;
|
|
2383
|
+
const manifestHash = hashManifest(manifest);
|
|
2384
|
+
|
|
2385
|
+
if (latestManifestVersion?.manifestHash === manifestHash) {
|
|
2386
|
+
return {
|
|
2387
|
+
version: latestManifestVersion.version,
|
|
2388
|
+
manifestHash,
|
|
2389
|
+
diff: null,
|
|
2390
|
+
noop: true,
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
const createdManifestVersion = await createAppManifestVersion({
|
|
2395
|
+
integrationId: input.integrationId,
|
|
2396
|
+
version: (latestManifestVersion?.version ?? 0) + 1,
|
|
2397
|
+
manifestHash,
|
|
2398
|
+
manifest,
|
|
2399
|
+
status: 'draft',
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
return {
|
|
2403
|
+
version: createdManifestVersion.version,
|
|
2404
|
+
manifestHash,
|
|
2405
|
+
diff: activeManifestVersion
|
|
2406
|
+
? diffManifests(activeManifestVersion.manifest, manifest)
|
|
2407
|
+
: null,
|
|
2408
|
+
noop: false,
|
|
2409
|
+
};
|
|
2410
|
+
}),
|
|
2411
|
+
|
|
2412
|
+
getManifestVersions: profileRoute
|
|
2413
|
+
.meta({
|
|
2414
|
+
openapi: {
|
|
2415
|
+
protect: true,
|
|
2416
|
+
method: 'GET',
|
|
2417
|
+
path: '/app-store/integration/{integrationId}/manifest/versions',
|
|
2418
|
+
tags: ['App Store'],
|
|
2419
|
+
summary: 'Get App Manifest Versions',
|
|
2420
|
+
description: 'List paginated app manifest versions for an integration.',
|
|
2421
|
+
},
|
|
2422
|
+
requiredScope: 'app-store:read',
|
|
2423
|
+
})
|
|
2424
|
+
.input(
|
|
2425
|
+
z.object({
|
|
2426
|
+
integrationId: z.string(),
|
|
2427
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
2428
|
+
cursor: z.string().optional(),
|
|
2429
|
+
})
|
|
2430
|
+
)
|
|
2431
|
+
.output(PaginatedAppManifestVersionsValidator)
|
|
2432
|
+
.query(async ({ input, ctx }) => {
|
|
2433
|
+
await verifyIntegrationOwnership(input.integrationId, ctx.user.profile.profileId);
|
|
2434
|
+
|
|
2435
|
+
const limit = input.limit ?? 25;
|
|
2436
|
+
const versions = await getManifestVersionsForIntegration(input.integrationId, {
|
|
2437
|
+
limit: limit + 1,
|
|
2438
|
+
cursor: input.cursor,
|
|
2439
|
+
});
|
|
2440
|
+
|
|
2441
|
+
const hasMore = versions.length > limit;
|
|
2442
|
+
const records = (hasMore ? versions.slice(0, limit) : versions).map(
|
|
2443
|
+
toManifestVersionSummary
|
|
2444
|
+
);
|
|
2445
|
+
|
|
2446
|
+
return {
|
|
2447
|
+
hasMore,
|
|
2448
|
+
cursor: hasMore ? String(records[records.length - 1]?.version) : undefined,
|
|
2449
|
+
records,
|
|
2450
|
+
};
|
|
2451
|
+
}),
|
|
2452
|
+
|
|
2453
|
+
getManifestVersion: profileRoute
|
|
2454
|
+
.meta({
|
|
2455
|
+
openapi: {
|
|
2456
|
+
protect: true,
|
|
2457
|
+
method: 'GET',
|
|
2458
|
+
path: '/app-store/integration/{integrationId}/manifest/{version}',
|
|
2459
|
+
tags: ['App Store'],
|
|
2460
|
+
summary: 'Get App Manifest Version',
|
|
2461
|
+
description: 'Get a full app manifest version for an integration.',
|
|
2462
|
+
},
|
|
2463
|
+
requiredScope: 'app-store:read',
|
|
2464
|
+
})
|
|
2465
|
+
.input(
|
|
2466
|
+
z.object({
|
|
2467
|
+
integrationId: z.string(),
|
|
2468
|
+
version: z.number().int().min(1),
|
|
2469
|
+
})
|
|
2470
|
+
)
|
|
2471
|
+
.output(AppManifestVersionValidator)
|
|
2472
|
+
.query(async ({ input, ctx }) => {
|
|
2473
|
+
await verifyIntegrationOwnership(input.integrationId, ctx.user.profile.profileId);
|
|
2474
|
+
|
|
2475
|
+
const manifestVersion = await getManifestVersionForIntegration(
|
|
2476
|
+
input.integrationId,
|
|
2477
|
+
input.version
|
|
2478
|
+
);
|
|
2479
|
+
|
|
2480
|
+
if (!manifestVersion) {
|
|
2481
|
+
throw new TRPCError({
|
|
2482
|
+
code: 'NOT_FOUND',
|
|
2483
|
+
message: 'App manifest version not found',
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
return manifestVersion;
|
|
2488
|
+
}),
|
|
2489
|
+
|
|
2490
|
+
getManifestDiff: profileRoute
|
|
2491
|
+
.meta({
|
|
2492
|
+
openapi: {
|
|
2493
|
+
protect: true,
|
|
2494
|
+
method: 'GET',
|
|
2495
|
+
path: '/app-store/integration/{integrationId}/manifest/diff/{toVersion}',
|
|
2496
|
+
tags: ['App Store'],
|
|
2497
|
+
summary: 'Get App Manifest Diff',
|
|
2498
|
+
description: 'Diff one app manifest version against another for an integration.',
|
|
2499
|
+
},
|
|
2500
|
+
requiredScope: 'app-store:read',
|
|
2501
|
+
})
|
|
2502
|
+
.input(
|
|
2503
|
+
z.object({
|
|
2504
|
+
integrationId: z.string(),
|
|
2505
|
+
fromVersion: z.number().int().min(1).optional(),
|
|
2506
|
+
toVersion: z.number().int().min(1),
|
|
2507
|
+
})
|
|
2508
|
+
)
|
|
2509
|
+
.output(AppManifestDiffValidator)
|
|
2510
|
+
.query(async ({ input, ctx }) => {
|
|
2511
|
+
await verifyIntegrationOwnership(input.integrationId, ctx.user.profile.profileId);
|
|
2512
|
+
|
|
2513
|
+
const toManifestVersion = await getManifestVersionForIntegration(
|
|
2514
|
+
input.integrationId,
|
|
2515
|
+
input.toVersion
|
|
2516
|
+
);
|
|
2517
|
+
|
|
2518
|
+
if (!toManifestVersion) {
|
|
2519
|
+
throw new TRPCError({
|
|
2520
|
+
code: 'NOT_FOUND',
|
|
2521
|
+
message: 'Target app manifest version not found',
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
const fromManifestVersion = input.fromVersion
|
|
2526
|
+
? await getManifestVersionForIntegration(input.integrationId, input.fromVersion)
|
|
2527
|
+
: await getActiveManifestVersionForIntegration(input.integrationId);
|
|
2528
|
+
|
|
2529
|
+
if (!fromManifestVersion) {
|
|
2530
|
+
throw new TRPCError({
|
|
2531
|
+
code: 'NOT_FOUND',
|
|
2532
|
+
message: 'Source app manifest version not found',
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
return diffManifests(fromManifestVersion.manifest, toManifestVersion.manifest);
|
|
2537
|
+
}),
|
|
2538
|
+
|
|
2539
|
+
applyManifestVersion: profileRoute
|
|
2540
|
+
.meta({
|
|
2541
|
+
openapi: {
|
|
2542
|
+
protect: true,
|
|
2543
|
+
method: 'POST',
|
|
2544
|
+
path: '/app-store/integration/{integrationId}/manifest/{version}/apply',
|
|
2545
|
+
tags: ['App Store'],
|
|
2546
|
+
summary: 'Apply App Manifest Version',
|
|
2547
|
+
description:
|
|
2548
|
+
'Mark an app manifest version active and idempotently reconcile derived listing entities.',
|
|
2549
|
+
},
|
|
2550
|
+
requiredScope: 'app-store:write',
|
|
2551
|
+
})
|
|
2552
|
+
.input(
|
|
2553
|
+
z.object({
|
|
2554
|
+
integrationId: z.string(),
|
|
2555
|
+
version: z.number().int().min(1),
|
|
2556
|
+
listingId: z.string().optional(),
|
|
2557
|
+
})
|
|
2558
|
+
)
|
|
2559
|
+
.output(ApplyManifestVersionResponseValidator)
|
|
2560
|
+
.mutation(async ({ input, ctx }) => {
|
|
2561
|
+
const integration = await verifyIntegrationOwnership(
|
|
2562
|
+
input.integrationId,
|
|
2563
|
+
ctx.user.profile.profileId
|
|
2564
|
+
);
|
|
2565
|
+
const manifestVersion = await getManifestVersionForIntegration(
|
|
2566
|
+
input.integrationId,
|
|
2567
|
+
input.version
|
|
2568
|
+
);
|
|
2569
|
+
|
|
2570
|
+
if (!manifestVersion) {
|
|
2571
|
+
throw new TRPCError({
|
|
2572
|
+
code: 'NOT_FOUND',
|
|
2573
|
+
message: 'App manifest version not found',
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
const listing = await resolveManifestApplyListing({
|
|
2578
|
+
integrationId: input.integrationId,
|
|
2579
|
+
listingId: input.listingId,
|
|
2580
|
+
profileId: ctx.user.profile.profileId,
|
|
2581
|
+
});
|
|
2582
|
+
const integrationOwner = await getOwnerProfileForIntegration(input.integrationId);
|
|
2583
|
+
|
|
2584
|
+
if (!integrationOwner) {
|
|
2585
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Integration owner not found' });
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
if (listing.app_listing_status === 'LISTED' && manifestVersion.status !== 'active') {
|
|
2589
|
+
// A live app's new capabilities wait for review instead of taking effect.
|
|
2590
|
+
await holdChangesForReview(listing, { manifestVersion: manifestVersion.version });
|
|
2591
|
+
|
|
2592
|
+
return {
|
|
2593
|
+
applied: false,
|
|
2594
|
+
pendingReview: true,
|
|
2595
|
+
version: manifestVersion.version,
|
|
2596
|
+
reconciled: {
|
|
2597
|
+
templatesUpserted: 0,
|
|
2598
|
+
templatesSkipped: 0,
|
|
2599
|
+
contractsUpserted: 0,
|
|
2600
|
+
contractsSkipped: 0,
|
|
2601
|
+
signingAuthorityEnsured: false,
|
|
2602
|
+
},
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
const reconciled = await reconcileManifestVersionForListing({
|
|
2607
|
+
ctx,
|
|
2608
|
+
integration,
|
|
2609
|
+
integrationOwner,
|
|
2610
|
+
listing,
|
|
2611
|
+
manifestVersion,
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2614
|
+
return { applied: true, version: manifestVersion.version, reconciled };
|
|
2615
|
+
}),
|
|
2616
|
+
|
|
1842
2617
|
updateListing: profileRoute
|
|
1843
2618
|
.meta({
|
|
1844
2619
|
openapi: {
|
|
@@ -1870,6 +2645,10 @@ export const appStoreRouter = t.router({
|
|
|
1870
2645
|
...input.updates,
|
|
1871
2646
|
});
|
|
1872
2647
|
|
|
2648
|
+
if (listing.app_listing_status === 'LISTED') {
|
|
2649
|
+
return holdChangesForReview(listing, { changes: storageUpdates });
|
|
2650
|
+
}
|
|
2651
|
+
|
|
1873
2652
|
if (!listing.slug) {
|
|
1874
2653
|
const displayName = storageUpdates.display_name ?? listing.display_name;
|
|
1875
2654
|
storageUpdates.slug = await getAvailableAppSlug(displayName, listing.listing_id);
|
|
@@ -2025,6 +2804,8 @@ export const appStoreRouter = t.router({
|
|
|
2025
2804
|
});
|
|
2026
2805
|
}
|
|
2027
2806
|
|
|
2807
|
+
assertLaunchConfigSubmittable(listing.launch_type, listing.launch_config_json);
|
|
2808
|
+
|
|
2028
2809
|
const submittedAt = new Date().toISOString();
|
|
2029
2810
|
|
|
2030
2811
|
// Update status on the listing node
|
|
@@ -2125,6 +2906,125 @@ export const appStoreRouter = t.router({
|
|
|
2125
2906
|
return result;
|
|
2126
2907
|
}),
|
|
2127
2908
|
|
|
2909
|
+
submitListingUpdate: profileRoute
|
|
2910
|
+
.meta({
|
|
2911
|
+
openapi: {
|
|
2912
|
+
protect: true,
|
|
2913
|
+
method: 'POST',
|
|
2914
|
+
path: '/app-store/listing/{listingId}/update/submit',
|
|
2915
|
+
tags: ['App Store'],
|
|
2916
|
+
summary: 'Submit Listing Update for Review',
|
|
2917
|
+
description:
|
|
2918
|
+
'Send pending changes to a LISTED app for admin review. The live listing is unchanged until approval.',
|
|
2919
|
+
},
|
|
2920
|
+
requiredScope: 'app-store:write',
|
|
2921
|
+
})
|
|
2922
|
+
.input(z.object({ listingId: z.string() }))
|
|
2923
|
+
.output(z.boolean())
|
|
2924
|
+
.mutation(async ({ input, ctx }) => {
|
|
2925
|
+
const { listing } = await verifyListingOwnership(
|
|
2926
|
+
input.listingId,
|
|
2927
|
+
ctx.user.profile.profileId
|
|
2928
|
+
);
|
|
2929
|
+
const pending = readPendingUpdate(listing);
|
|
2930
|
+
|
|
2931
|
+
if (listing.app_listing_status !== 'LISTED' || !pending) {
|
|
2932
|
+
throw new TRPCError({
|
|
2933
|
+
code: 'BAD_REQUEST',
|
|
2934
|
+
message: 'There are no changes to submit for this app.',
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
if (listing.pending_update_status === 'PENDING_REVIEW') {
|
|
2938
|
+
throw new TRPCError({
|
|
2939
|
+
code: 'BAD_REQUEST',
|
|
2940
|
+
message: PENDING_UPDATE_IN_REVIEW_MESSAGE,
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
const nextLaunchType = pending.changes.launch_type ?? listing.launch_type;
|
|
2945
|
+
const nextLaunchConfig =
|
|
2946
|
+
pending.changes.launch_config_json ?? listing.launch_config_json;
|
|
2947
|
+
assertLaunchConfigSubmittable(String(nextLaunchType), String(nextLaunchConfig));
|
|
2948
|
+
|
|
2949
|
+
const result = await updateAppStoreListing(listing, {
|
|
2950
|
+
pending_update_status: 'PENDING_REVIEW',
|
|
2951
|
+
pending_update_submitted_at: new Date().toISOString(),
|
|
2952
|
+
});
|
|
2953
|
+
|
|
2954
|
+
await notifyAppStoreAdmins(ctx.user.profile, 'APP_LISTING_SUBMITTED', listing, {
|
|
2955
|
+
isUpdate: true,
|
|
2956
|
+
});
|
|
2957
|
+
|
|
2958
|
+
return result;
|
|
2959
|
+
}),
|
|
2960
|
+
|
|
2961
|
+
withdrawListingUpdate: profileRoute
|
|
2962
|
+
.meta({
|
|
2963
|
+
openapi: {
|
|
2964
|
+
protect: true,
|
|
2965
|
+
method: 'POST',
|
|
2966
|
+
path: '/app-store/listing/{listingId}/update/withdraw',
|
|
2967
|
+
tags: ['App Store'],
|
|
2968
|
+
summary: 'Withdraw Listing Update from Review',
|
|
2969
|
+
description: 'Return a submitted update to a LISTED app back to editable changes.',
|
|
2970
|
+
},
|
|
2971
|
+
requiredScope: 'app-store:write',
|
|
2972
|
+
})
|
|
2973
|
+
.input(z.object({ listingId: z.string() }))
|
|
2974
|
+
.output(z.boolean())
|
|
2975
|
+
.mutation(async ({ input, ctx }) => {
|
|
2976
|
+
const { listing } = await verifyListingOwnership(
|
|
2977
|
+
input.listingId,
|
|
2978
|
+
ctx.user.profile.profileId
|
|
2979
|
+
);
|
|
2980
|
+
|
|
2981
|
+
if (listing.pending_update_status !== 'PENDING_REVIEW') {
|
|
2982
|
+
throw new TRPCError({
|
|
2983
|
+
code: 'BAD_REQUEST',
|
|
2984
|
+
message: 'There is no update in review for this app.',
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
const result = await updateAppStoreListing(listing, { pending_update_status: 'DRAFT' });
|
|
2989
|
+
|
|
2990
|
+
await notifyAppStoreAdmins(ctx.user.profile, 'APP_LISTING_WITHDRAWN', listing, {
|
|
2991
|
+
isUpdate: true,
|
|
2992
|
+
});
|
|
2993
|
+
|
|
2994
|
+
return result;
|
|
2995
|
+
}),
|
|
2996
|
+
|
|
2997
|
+
discardListingUpdate: profileRoute
|
|
2998
|
+
.meta({
|
|
2999
|
+
openapi: {
|
|
3000
|
+
protect: true,
|
|
3001
|
+
method: 'POST',
|
|
3002
|
+
path: '/app-store/listing/{listingId}/update/discard',
|
|
3003
|
+
tags: ['App Store'],
|
|
3004
|
+
summary: 'Discard Listing Update',
|
|
3005
|
+
description: 'Throw away unsubmitted changes to a LISTED app.',
|
|
3006
|
+
},
|
|
3007
|
+
requiredScope: 'app-store:write',
|
|
3008
|
+
})
|
|
3009
|
+
.input(z.object({ listingId: z.string() }))
|
|
3010
|
+
.output(z.boolean())
|
|
3011
|
+
.mutation(async ({ input, ctx }) => {
|
|
3012
|
+
const { listing } = await verifyListingOwnership(
|
|
3013
|
+
input.listingId,
|
|
3014
|
+
ctx.user.profile.profileId
|
|
3015
|
+
);
|
|
3016
|
+
|
|
3017
|
+
if (listing.pending_update_status === 'PENDING_REVIEW') {
|
|
3018
|
+
throw new TRPCError({
|
|
3019
|
+
code: 'BAD_REQUEST',
|
|
3020
|
+
message: PENDING_UPDATE_IN_REVIEW_MESSAGE,
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
await clearPendingListingUpdate(listing.listing_id);
|
|
3025
|
+
return true;
|
|
3026
|
+
}),
|
|
3027
|
+
|
|
2128
3028
|
deleteListing: profileRoute
|
|
2129
3029
|
.meta({
|
|
2130
3030
|
openapi: {
|
|
@@ -2133,14 +3033,34 @@ export const appStoreRouter = t.router({
|
|
|
2133
3033
|
path: '/app-store/listing/{listingId}',
|
|
2134
3034
|
tags: ['App Store'],
|
|
2135
3035
|
summary: 'Delete App Store Listing',
|
|
2136
|
-
description:
|
|
3036
|
+
description:
|
|
3037
|
+
'Delete an App Store Listing. Only draft or rejected listings may be deleted; in-review and live listings must be withdrawn or removed from the store first.',
|
|
2137
3038
|
},
|
|
2138
3039
|
requiredScope: 'app-store:delete',
|
|
2139
3040
|
})
|
|
2140
3041
|
.input(z.object({ listingId: z.string() }))
|
|
2141
3042
|
.output(z.boolean())
|
|
2142
3043
|
.mutation(async ({ input, ctx }) => {
|
|
2143
|
-
await verifyListingOwnership(
|
|
3044
|
+
const { listing } = await verifyListingOwnership(
|
|
3045
|
+
input.listingId,
|
|
3046
|
+
ctx.user.profile.profileId
|
|
3047
|
+
);
|
|
3048
|
+
|
|
3049
|
+
// ARCHIVED is how a rejected listing is represented (there is no
|
|
3050
|
+
// separate REJECTED status), so DRAFT and ARCHIVED are deletable.
|
|
3051
|
+
if (listing.app_listing_status === 'PENDING_REVIEW') {
|
|
3052
|
+
throw new TRPCError({
|
|
3053
|
+
code: 'PRECONDITION_FAILED',
|
|
3054
|
+
message: 'Only draft apps can be deleted. Withdraw it from review first.',
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
if (listing.app_listing_status === 'LISTED') {
|
|
3059
|
+
throw new TRPCError({
|
|
3060
|
+
code: 'PRECONDITION_FAILED',
|
|
3061
|
+
message: "Live apps can't be deleted. Remove it from the store first.",
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
2144
3064
|
|
|
2145
3065
|
await deleteAppStoreListing(input.listingId);
|
|
2146
3066
|
|
|
@@ -2838,6 +3758,10 @@ export const appStoreRouter = t.router({
|
|
|
2838
3758
|
);
|
|
2839
3759
|
}
|
|
2840
3760
|
|
|
3761
|
+
if (eventType === 'upsert-consent-contract') {
|
|
3762
|
+
return handleUpsertConsentContractEvent(ctx, listing, resolvedListingId, event);
|
|
3763
|
+
}
|
|
3764
|
+
|
|
2841
3765
|
if (eventType === 'send-ai-session-credential') {
|
|
2842
3766
|
return handleSendAiSessionCredentialEvent(ctx, profile, resolvedListingId, event);
|
|
2843
3767
|
}
|
|
@@ -2947,6 +3871,98 @@ export const appStoreRouter = t.router({
|
|
|
2947
3871
|
return result;
|
|
2948
3872
|
}),
|
|
2949
3873
|
|
|
3874
|
+
adminReviewListingUpdate: profileRoute
|
|
3875
|
+
.meta({
|
|
3876
|
+
openapi: {
|
|
3877
|
+
protect: true,
|
|
3878
|
+
method: 'POST',
|
|
3879
|
+
path: '/app-store/admin/listing/{listingId}/update/review',
|
|
3880
|
+
tags: ['App Store Admin'],
|
|
3881
|
+
summary: 'Review Listing Update (Admin)',
|
|
3882
|
+
description:
|
|
3883
|
+
'Approve (apply to the live listing) or reject a submitted update to a LISTED app.',
|
|
3884
|
+
},
|
|
3885
|
+
requiredScope: 'app-store:admin',
|
|
3886
|
+
})
|
|
3887
|
+
.input(z.object({ listingId: z.string(), approve: z.boolean() }))
|
|
3888
|
+
.output(z.boolean())
|
|
3889
|
+
.mutation(async ({ input, ctx }) => {
|
|
3890
|
+
verifyAppStoreAdmin(ctx.user.profile.profileId);
|
|
3891
|
+
|
|
3892
|
+
const listing = await getListingOrThrow(input.listingId);
|
|
3893
|
+
const pending = readPendingUpdate(listing);
|
|
3894
|
+
|
|
3895
|
+
if (listing.pending_update_status !== 'PENDING_REVIEW' || !pending) {
|
|
3896
|
+
throw new TRPCError({
|
|
3897
|
+
code: 'BAD_REQUEST',
|
|
3898
|
+
message: 'This app has no update waiting for review.',
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
const integration = await getIntegrationForListing(input.listingId);
|
|
3903
|
+
const ownerProfile = integration
|
|
3904
|
+
? await getOwnerProfileForIntegration(integration.id)
|
|
3905
|
+
: null;
|
|
3906
|
+
|
|
3907
|
+
if (input.approve) {
|
|
3908
|
+
if (pending.manifestVersion !== undefined) {
|
|
3909
|
+
if (!integration || !ownerProfile) {
|
|
3910
|
+
throw new TRPCError({
|
|
3911
|
+
code: 'NOT_FOUND',
|
|
3912
|
+
message: 'Integration for this listing was not found',
|
|
3913
|
+
});
|
|
3914
|
+
}
|
|
3915
|
+
const manifestVersion = await getManifestVersionForIntegration(
|
|
3916
|
+
integration.id,
|
|
3917
|
+
pending.manifestVersion
|
|
3918
|
+
);
|
|
3919
|
+
if (manifestVersion) {
|
|
3920
|
+
await reconcileManifestVersionForListing({
|
|
3921
|
+
ctx,
|
|
3922
|
+
integration,
|
|
3923
|
+
integrationOwner: ownerProfile,
|
|
3924
|
+
listing,
|
|
3925
|
+
manifestVersion,
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
await updateAppStoreListing(listing, pending.changes as AppStoreListingUpdateType);
|
|
3931
|
+
await clearPendingListingUpdate(listing.listing_id);
|
|
3932
|
+
} else {
|
|
3933
|
+
await updateAppStoreListing(listing, { pending_update_status: 'DRAFT' });
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
if (ownerProfile) {
|
|
3937
|
+
const approvedName =
|
|
3938
|
+
typeof pending.changes.display_name === 'string' && input.approve
|
|
3939
|
+
? pending.changes.display_name
|
|
3940
|
+
: listing.display_name;
|
|
3941
|
+
|
|
3942
|
+
await addNotificationToQueue({
|
|
3943
|
+
type: input.approve
|
|
3944
|
+
? LCNNotificationTypeEnumValidator.enum.APP_LISTING_APPROVED
|
|
3945
|
+
: LCNNotificationTypeEnumValidator.enum.APP_LISTING_REJECTED,
|
|
3946
|
+
to: ownerProfile,
|
|
3947
|
+
from: ctx.user.profile,
|
|
3948
|
+
message: getNotificationMessage(
|
|
3949
|
+
input.approve ? 'appListingApproved' : 'appListingRejected',
|
|
3950
|
+
resolveRecipientLocale(ownerProfile),
|
|
3951
|
+
{ displayName: approvedName }
|
|
3952
|
+
),
|
|
3953
|
+
data: {
|
|
3954
|
+
metadata: {
|
|
3955
|
+
listingId: listing.listing_id,
|
|
3956
|
+
listingName: approvedName,
|
|
3957
|
+
isUpdate: true,
|
|
3958
|
+
},
|
|
3959
|
+
},
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
return true;
|
|
3964
|
+
}),
|
|
3965
|
+
|
|
2950
3966
|
adminUpdatePromotionLevel: profileRoute
|
|
2951
3967
|
.meta({
|
|
2952
3968
|
openapi: {
|
|
@@ -2992,6 +4008,7 @@ export const appStoreRouter = t.router({
|
|
|
2992
4008
|
limit: z.number().optional(),
|
|
2993
4009
|
cursor: z.string().optional(),
|
|
2994
4010
|
status: AppListingStatus.optional(),
|
|
4011
|
+
pendingUpdatesOnly: z.boolean().optional(),
|
|
2995
4012
|
})
|
|
2996
4013
|
.optional()
|
|
2997
4014
|
)
|
|
@@ -3009,6 +4026,7 @@ export const appStoreRouter = t.router({
|
|
|
3009
4026
|
status: input?.status,
|
|
3010
4027
|
includeAllStatuses: !input?.status, // Include all if no specific status filter
|
|
3011
4028
|
excludeDemoted: false, // Admin can see demoted listings
|
|
4029
|
+
pendingUpdatesOnly: input?.pendingUpdatesOnly,
|
|
3012
4030
|
});
|
|
3013
4031
|
|
|
3014
4032
|
const hasMore = results.length > limit;
|