keri-ts 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -2
- package/esm/keri/npm/src/acdc/authoring.js +195 -0
- package/esm/keri/npm/src/acdc/custody.js +187 -0
- package/esm/keri/npm/src/acdc/dhking.js +244 -32
- package/esm/keri/npm/src/acdc/regbasing.js +10 -0
- package/esm/keri/npm/src/acdc/salting.js +16 -1
- package/esm/keri/npm/src/acdc/scheming.js +93 -54
- package/esm/keri/npm/src/acdc/unblinding.js +118 -95
- package/esm/keri/npm/src/app/bootstrap/authenticating.js +4 -6
- package/esm/keri/npm/src/app/bootstrap/oobi-resolving.js +41 -0
- package/esm/keri/npm/src/app/bootstrap/oobiery.js +4 -6
- package/esm/keri/npm/src/app/credentials/credential-revoking.js +1 -0
- package/esm/keri/npm/src/app/credentials/credential-workflows.js +2 -0
- package/esm/keri/npm/src/app/credentials/registry-settling.js +44 -0
- package/esm/keri/npm/src/app/credentials/root-replicating.js +68 -60
- package/esm/keri/npm/src/app/credentials/root-replicator.js +198 -0
- package/esm/keri/npm/src/app/credentials/tel-completion.js +36 -0
- package/esm/keri/npm/src/app/delivery/forwarding.js +16 -3
- package/esm/keri/npm/src/app/exchanges/exchanging.js +14 -4
- package/esm/keri/npm/src/app/identity/challenging.js +50 -0
- package/esm/keri/npm/src/app/identity/delegating.js +77 -77
- package/esm/keri/npm/src/app/identity/delegation-approval.js +333 -0
- package/esm/keri/npm/src/app/identity/event-completion.js +154 -0
- package/esm/keri/npm/src/app/identity/habbing.js +129 -6
- package/esm/keri/npm/src/app/identity/keeping.js +77 -4
- package/esm/keri/npm/src/app/ipex/group-ipex.js +341 -0
- package/esm/keri/npm/src/app/ipex/group-issuance.js +905 -0
- package/esm/keri/npm/src/app/ipex/ipex-admitting.js +120 -0
- package/esm/keri/npm/src/app/ipex/ipex-conversation.js +197 -0
- package/esm/keri/npm/src/app/ipex/ipex-discloser.js +591 -0
- package/esm/keri/npm/src/app/ipex/ipex-granting.js +93 -0
- package/esm/keri/npm/src/app/ipex/ipex-grouping.js +50 -19
- package/esm/keri/npm/src/app/ipex/ipex-node-authentication.js +42 -18
- package/esm/keri/npm/src/app/ipex/issuance.js +896 -0
- package/esm/keri/npm/src/app/ipex/pending-presentations.js +46 -0
- package/esm/keri/npm/src/app/ipex/presentation-files.js +200 -0
- package/esm/keri/npm/src/app/ipex/presenting.js +408 -0
- package/esm/keri/npm/src/app/ipex/receive-issuance.js +86 -0
- package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +667 -0
- package/esm/keri/npm/src/app/mailbox/ipex-arrival.js +103 -0
- package/esm/keri/npm/src/app/mailbox/kel-publisher.js +322 -0
- package/esm/keri/npm/src/app/mailbox/kel-publishing.js +65 -0
- package/esm/keri/npm/src/app/mailbox/kel-submission.js +24 -0
- package/esm/keri/npm/src/app/mailbox/mailbox-admin.js +208 -0
- package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +212 -47
- package/esm/keri/npm/src/app/mailbox/mailboxing.js +10 -5
- package/esm/keri/npm/src/app/multisig/counseling.js +488 -0
- package/esm/keri/npm/src/app/multisig/group-members.js +18 -0
- package/esm/keri/npm/src/app/multisig/multisig-messaging.js +33 -6
- package/esm/keri/npm/src/app/multisig/multisig-workflows.js +98 -147
- package/esm/keri/npm/src/app/multisig/proposals.js +281 -0
- package/esm/keri/npm/src/app/multisig/proposing.js +122 -0
- package/esm/keri/npm/src/app/notifications/signaling.js +5 -5
- package/esm/keri/npm/src/app/protocol/parsering.js +26 -1
- package/esm/keri/npm/src/app/protocol/protocol-processing.js +9 -3
- package/esm/keri/npm/src/app/queries/query-workflows.js +98 -0
- package/esm/keri/npm/src/app/runtime/agent-runtime.js +106 -55
- package/esm/keri/npm/src/app/runtime/cue-runtime.js +3 -6
- package/esm/keri/npm/src/app/runtime/delivery-outbox.js +87 -0
- package/esm/keri/npm/src/app/runtime/doing.js +17 -0
- package/esm/keri/npm/src/app/runtime/ipex-auxiliaries.js +207 -0
- package/esm/keri/npm/src/app/runtime/operation-owners.js +151 -0
- package/esm/keri/npm/src/app/runtime/retry-backoff.js +93 -0
- package/esm/keri/npm/src/app/runtime/workflow-contract.js +80 -0
- package/esm/keri/npm/src/app/transport/http-client.js +25 -6
- package/esm/keri/npm/src/app/witness/http-endpoints.js +4 -4
- package/esm/keri/npm/src/cli/common/event-completion.js +49 -0
- package/esm/keri/npm/src/cli/common/runtime-workers.js +23 -0
- package/esm/keri/npm/src/cli/credentials/ipex.js +164 -191
- package/esm/keri/npm/src/cli/credentials/vc.js +53 -72
- package/esm/keri/npm/src/cli/endpoints/did.js +12 -3
- package/esm/keri/npm/src/cli/endpoints/oobi.js +10 -13
- package/esm/keri/npm/src/cli/identity/challenge.js +8 -34
- package/esm/keri/npm/src/cli/identity/delegate.js +81 -258
- package/esm/keri/npm/src/cli/identity/incept.js +15 -32
- package/esm/keri/npm/src/cli/identity/interact.js +5 -17
- package/esm/keri/npm/src/cli/identity/rotate.js +8 -36
- package/esm/keri/npm/src/cli/index.js +3 -1
- package/esm/keri/npm/src/cli/messaging/group-join.js +83 -0
- package/esm/keri/npm/src/cli/messaging/group-wait.js +88 -0
- package/esm/keri/npm/src/cli/messaging/multisig.js +190 -285
- package/esm/keri/npm/src/cli/messaging/root-copy-lines.js +31 -0
- package/esm/keri/npm/src/core/kel/eventing.js +66 -10
- package/esm/keri/npm/src/core/mailbox/kram-policy.js +39 -6
- package/esm/keri/npm/src/core/mailbox/kraming.js +2 -0
- package/esm/keri/npm/src/core/records.js +23 -0
- package/esm/keri/npm/src/core/routing.js +13 -0
- package/esm/keri/npm/src/db/basing.js +34 -2
- package/esm/keri/npm/src/db/core/indexeddb-persistence.js +221 -0
- package/esm/keri/npm/src/db/indexeddb.js +98 -0
- package/esm/keri/npm/src/db/keeping.js +11 -2
- package/esm/keri/npm/src/db/store-lock.js +150 -0
- package/esm/keri/npm/src/npm/db-indexeddb.browser.js +2 -0
- package/esm/keri/npm/src/npm/db-indexeddb.js +3 -0
- package/esm/keri/npm/src/npm/db.browser.js +2 -0
- package/esm/keri/npm/src/npm/index.browser.js +9 -0
- package/esm/keri/npm/src/npm/runtime-web.browser.js +2 -0
- package/esm/keri/npm/src/npm/runtime-web.js +3 -0
- package/esm/keri/npm/src/npm/runtime.browser.js +2 -0
- package/esm/keri/npm/src/runtime/index.js +51 -4
- package/esm/keri/npm/src/runtime/web/output-barrier.js +47 -0
- package/esm/keri/npm/src/runtime/web/persistent-storage.js +37 -0
- package/esm/keri/npm/src/runtime/web.js +10 -0
- package/esm/keri/npm/src/vdr/tel-publication.js +16 -0
- package/esm/keri/npm/src/version.js +2 -2
- package/package.json +15 -2
- package/types/acdc/authoring.d.ts +54 -0
- package/types/acdc/authoring.d.ts.map +1 -0
- package/types/acdc/custody.d.ts +84 -0
- package/types/acdc/custody.d.ts.map +1 -0
- package/types/acdc/dhking.d.ts +94 -3
- package/types/acdc/dhking.d.ts.map +1 -1
- package/types/acdc/regbasing.d.ts +4 -0
- package/types/acdc/regbasing.d.ts.map +1 -1
- package/types/acdc/salting.d.ts +6 -1
- package/types/acdc/salting.d.ts.map +1 -1
- package/types/acdc/scheming.d.ts +2 -0
- package/types/acdc/scheming.d.ts.map +1 -1
- package/types/acdc/unblinding.d.ts +58 -19
- package/types/acdc/unblinding.d.ts.map +1 -1
- package/types/app/bootstrap/authenticating.d.ts +3 -1
- package/types/app/bootstrap/authenticating.d.ts.map +1 -1
- package/types/app/bootstrap/oobi-resolving.d.ts +92 -0
- package/types/app/bootstrap/oobi-resolving.d.ts.map +1 -0
- package/types/app/bootstrap/oobiery.d.ts +3 -1
- package/types/app/bootstrap/oobiery.d.ts.map +1 -1
- package/types/app/credentials/credential-revoking.d.ts +3 -0
- package/types/app/credentials/credential-revoking.d.ts.map +1 -1
- package/types/app/credentials/credential-workflows.d.ts +5 -0
- package/types/app/credentials/credential-workflows.d.ts.map +1 -1
- package/types/app/credentials/registry-settling.d.ts +85 -0
- package/types/app/credentials/registry-settling.d.ts.map +1 -0
- package/types/app/credentials/root-replicating.d.ts +56 -44
- package/types/app/credentials/root-replicating.d.ts.map +1 -1
- package/types/app/credentials/root-replicator.d.ts +105 -0
- package/types/app/credentials/root-replicator.d.ts.map +1 -0
- package/types/app/credentials/tel-completion.d.ts +55 -0
- package/types/app/credentials/tel-completion.d.ts.map +1 -0
- package/types/app/delivery/forwarding.d.ts +6 -0
- package/types/app/delivery/forwarding.d.ts.map +1 -1
- package/types/app/exchanges/exchanging.d.ts.map +1 -1
- package/types/app/identity/challenging.d.ts +54 -0
- package/types/app/identity/challenging.d.ts.map +1 -1
- package/types/app/identity/delegating.d.ts +21 -4
- package/types/app/identity/delegating.d.ts.map +1 -1
- package/types/app/identity/delegation-approval.d.ts +228 -0
- package/types/app/identity/delegation-approval.d.ts.map +1 -0
- package/types/app/identity/event-completion.d.ts +151 -0
- package/types/app/identity/event-completion.d.ts.map +1 -0
- package/types/app/identity/habbing.d.ts +67 -2
- package/types/app/identity/habbing.d.ts.map +1 -1
- package/types/app/identity/keeping.d.ts +49 -3
- package/types/app/identity/keeping.d.ts.map +1 -1
- package/types/app/ipex/group-ipex.d.ts +254 -0
- package/types/app/ipex/group-ipex.d.ts.map +1 -0
- package/types/app/ipex/group-issuance.d.ts +281 -0
- package/types/app/ipex/group-issuance.d.ts.map +1 -0
- package/types/app/ipex/ipex-admitting.d.ts +47 -0
- package/types/app/ipex/ipex-admitting.d.ts.map +1 -0
- package/types/app/ipex/ipex-conversation.d.ts +195 -0
- package/types/app/ipex/ipex-conversation.d.ts.map +1 -0
- package/types/app/ipex/ipex-discloser.d.ts +156 -0
- package/types/app/ipex/ipex-discloser.d.ts.map +1 -0
- package/types/app/ipex/ipex-granting.d.ts +145 -0
- package/types/app/ipex/ipex-granting.d.ts.map +1 -0
- package/types/app/ipex/ipex-grouping.d.ts +18 -0
- package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
- package/types/app/ipex/ipex-node-authentication.d.ts +9 -1
- package/types/app/ipex/ipex-node-authentication.d.ts.map +1 -1
- package/types/app/ipex/issuance.d.ts +445 -0
- package/types/app/ipex/issuance.d.ts.map +1 -0
- package/types/app/ipex/pending-presentations.d.ts +25 -0
- package/types/app/ipex/pending-presentations.d.ts.map +1 -0
- package/types/app/ipex/presentation-files.d.ts +47 -0
- package/types/app/ipex/presentation-files.d.ts.map +1 -0
- package/types/app/ipex/presenting.d.ts +300 -0
- package/types/app/ipex/presenting.d.ts.map +1 -0
- package/types/app/ipex/receive-issuance.d.ts +99 -0
- package/types/app/ipex/receive-issuance.d.ts.map +1 -0
- package/types/app/ipex/unblinding-workflow.d.ts +257 -0
- package/types/app/ipex/unblinding-workflow.d.ts.map +1 -0
- package/types/app/mailbox/ipex-arrival.d.ts +104 -0
- package/types/app/mailbox/ipex-arrival.d.ts.map +1 -0
- package/types/app/mailbox/kel-publisher.d.ts +141 -0
- package/types/app/mailbox/kel-publisher.d.ts.map +1 -0
- package/types/app/mailbox/kel-publishing.d.ts +70 -0
- package/types/app/mailbox/kel-publishing.d.ts.map +1 -0
- package/types/app/mailbox/kel-submission.d.ts +18 -0
- package/types/app/mailbox/kel-submission.d.ts.map +1 -0
- package/types/app/mailbox/mailbox-admin.d.ts +113 -0
- package/types/app/mailbox/mailbox-admin.d.ts.map +1 -0
- package/types/app/mailbox/mailbox-poller.d.ts +92 -3
- package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
- package/types/app/mailbox/mailboxing.d.ts +3 -0
- package/types/app/mailbox/mailboxing.d.ts.map +1 -1
- package/types/app/multisig/counseling.d.ts +196 -0
- package/types/app/multisig/counseling.d.ts.map +1 -0
- package/types/app/multisig/group-members.d.ts +11 -0
- package/types/app/multisig/group-members.d.ts.map +1 -1
- package/types/app/multisig/grouping.d.ts +39 -20
- package/types/app/multisig/grouping.d.ts.map +1 -1
- package/types/app/multisig/multisig-messaging.d.ts +23 -6
- package/types/app/multisig/multisig-messaging.d.ts.map +1 -1
- package/types/app/multisig/multisig-workflows.d.ts +91 -22
- package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
- package/types/app/multisig/proposals.d.ts +129 -0
- package/types/app/multisig/proposals.d.ts.map +1 -0
- package/types/app/multisig/proposing.d.ts +125 -0
- package/types/app/multisig/proposing.d.ts.map +1 -0
- package/types/app/notifications/signaling.d.ts +3 -1
- package/types/app/notifications/signaling.d.ts.map +1 -1
- package/types/app/protocol/parsering.d.ts +13 -0
- package/types/app/protocol/parsering.d.ts.map +1 -1
- package/types/app/protocol/protocol-processing.d.ts +7 -1
- package/types/app/protocol/protocol-processing.d.ts.map +1 -1
- package/types/app/queries/query-workflows.d.ts +98 -0
- package/types/app/queries/query-workflows.d.ts.map +1 -0
- package/types/app/runtime/agent-runtime.d.ts +72 -25
- package/types/app/runtime/agent-runtime.d.ts.map +1 -1
- package/types/app/runtime/cue-runtime.d.ts +2 -1
- package/types/app/runtime/cue-runtime.d.ts.map +1 -1
- package/types/app/runtime/delivery-outbox.d.ts +68 -0
- package/types/app/runtime/delivery-outbox.d.ts.map +1 -0
- package/types/app/runtime/doing.d.ts +27 -0
- package/types/app/runtime/doing.d.ts.map +1 -0
- package/types/app/runtime/ipex-auxiliaries.d.ts +74 -0
- package/types/app/runtime/ipex-auxiliaries.d.ts.map +1 -0
- package/types/app/runtime/operation-owners.d.ts +137 -0
- package/types/app/runtime/operation-owners.d.ts.map +1 -0
- package/types/app/runtime/retry-backoff.d.ts +49 -0
- package/types/app/runtime/retry-backoff.d.ts.map +1 -0
- package/types/app/runtime/workflow-contract.d.ts +79 -0
- package/types/app/runtime/workflow-contract.d.ts.map +1 -0
- package/types/app/transport/http-client.d.ts.map +1 -1
- package/types/cli/common/event-completion.d.ts +26 -0
- package/types/cli/common/event-completion.d.ts.map +1 -0
- package/types/cli/common/runtime-workers.d.ts +23 -0
- package/types/cli/common/runtime-workers.d.ts.map +1 -0
- package/types/cli/credentials/ipex.d.ts.map +1 -1
- package/types/cli/credentials/vc.d.ts +1 -1
- package/types/cli/credentials/vc.d.ts.map +1 -1
- package/types/cli/endpoints/did.d.ts.map +1 -1
- package/types/cli/endpoints/oobi.d.ts +3 -1
- package/types/cli/endpoints/oobi.d.ts.map +1 -1
- package/types/cli/identity/challenge.d.ts +1 -1
- package/types/cli/identity/challenge.d.ts.map +1 -1
- package/types/cli/identity/delegate.d.ts +14 -1
- package/types/cli/identity/delegate.d.ts.map +1 -1
- package/types/cli/identity/incept.d.ts.map +1 -1
- package/types/cli/identity/interact.d.ts.map +1 -1
- package/types/cli/identity/rotate.d.ts.map +1 -1
- package/types/cli/index.d.ts +3 -1
- package/types/cli/index.d.ts.map +1 -1
- package/types/cli/messaging/group-join.d.ts +56 -0
- package/types/cli/messaging/group-join.d.ts.map +1 -0
- package/types/cli/messaging/group-wait.d.ts +34 -0
- package/types/cli/messaging/group-wait.d.ts.map +1 -0
- package/types/cli/messaging/multisig.d.ts +29 -5
- package/types/cli/messaging/multisig.d.ts.map +1 -1
- package/types/cli/messaging/root-copy-lines.d.ts +39 -0
- package/types/cli/messaging/root-copy-lines.d.ts.map +1 -0
- package/types/core/cues.d.ts +11 -0
- package/types/core/cues.d.ts.map +1 -1
- package/types/core/kel/eventing.d.ts.map +1 -1
- package/types/core/mailbox/kram-policy.d.ts +14 -0
- package/types/core/mailbox/kram-policy.d.ts.map +1 -1
- package/types/core/mailbox/kraming.d.ts +1 -1
- package/types/core/mailbox/kraming.d.ts.map +1 -1
- package/types/core/records.d.ts +61 -0
- package/types/core/records.d.ts.map +1 -1
- package/types/core/routing.d.ts +6 -0
- package/types/core/routing.d.ts.map +1 -1
- package/types/db/basing.d.ts +8 -2
- package/types/db/basing.d.ts.map +1 -1
- package/types/db/core/indexeddb-persistence.d.ts +59 -0
- package/types/db/core/indexeddb-persistence.d.ts.map +1 -0
- package/types/db/indexeddb.d.ts +37 -0
- package/types/db/indexeddb.d.ts.map +1 -0
- package/types/db/keeping.d.ts +5 -2
- package/types/db/keeping.d.ts.map +1 -1
- package/types/db/store-lock.d.ts +72 -0
- package/types/db/store-lock.d.ts.map +1 -0
- package/types/npm/db-indexeddb.d.ts +4 -0
- package/types/npm/db-indexeddb.d.ts.map +1 -0
- package/types/npm/runtime-web.d.ts +4 -0
- package/types/npm/runtime-web.d.ts.map +1 -0
- package/types/runtime/index.d.ts +42 -7
- package/types/runtime/index.d.ts.map +1 -1
- package/types/runtime/web/output-barrier.d.ts +30 -0
- package/types/runtime/web/output-barrier.d.ts.map +1 -0
- package/types/runtime/web/persistent-storage.d.ts +36 -0
- package/types/runtime/web/persistent-storage.d.ts.map +1 -0
- package/types/runtime/web.d.ts +11 -0
- package/types/runtime/web.d.ts.map +1 -0
- package/types/vdr/tel-publication.d.ts +6 -0
- package/types/vdr/tel-publication.d.ts.map +1 -1
- package/types/version.d.ts +2 -2
- package/esm/keri/npm/src/cli/common/tel-completion.js +0 -24
- package/types/cli/common/tel-completion.d.ts +0 -8
- package/types/cli/common/tel-completion.d.ts.map +0 -1
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-owned operations for single-signature IPEX Unblind (proof release for one live presentation).
|
|
3
|
+
*
|
|
4
|
+
* One explicit consent step, the disclosee's. The disclosee calls `requestUnblind`: it approves the exact live
|
|
5
|
+
* Grant, signs the query for the missing public suffix and returns after local commit. The discloser has no
|
|
6
|
+
* approval step: it consented by signing the Grant, so an owner that authenticated a peer query for a live
|
|
7
|
+
* Grant its identifier sent replies on its own, and `processOnce` adopts and delivers that reply under the
|
|
8
|
+
* Grant's deadline (KERIpy keeps a local approve gate; the wire is unchanged).
|
|
9
|
+
* `UnblindingOperations.processOnce` also sends and resends the unchanged query each second, finishes the
|
|
10
|
+
* Grant once the authenticated reply is complete, and ends the operation at the Grant's original exchange
|
|
11
|
+
* deadline. The only signing outside the Grant's own scope is the requester's query. A record that is active
|
|
12
|
+
* with no live operation reads `interrupted` (or `expired` past the deadline) and nothing is sent for it
|
|
13
|
+
* after a restart; a repeated peer query is answered again from stored state.
|
|
14
|
+
*
|
|
15
|
+
* Workflow-contract shape (`app/runtime/workflow-contract.ts`): `requestUnblind` returns after local
|
|
16
|
+
* commit and never wait for the peer, `unblindingStatus` is a pure read, `waitForUnblinding` only observes
|
|
17
|
+
* and returns a `WaitResult`, `abandonUnblinding` is local. Completion creates no Admit.
|
|
18
|
+
*
|
|
19
|
+
* There is no operation store; status is the live table plus what the stores prove. Durable: the Grant and
|
|
20
|
+
* its deadline, and the finished proof view. A requester whose Grant's proofs are current reads `done`; one
|
|
21
|
+
* whose Grant is still live with proofs missing and no live operation reads `interrupted` (request it
|
|
22
|
+
* again), and past the deadline `expired`. A responder is driven entirely by a peer's query, so a
|
|
23
|
+
* responder's operation exists only while live: its `done`, `failed` and `expired` readings, like every
|
|
24
|
+
* requester `abandoned` and `failed` reading, are remembered only until the process ends, and a responder
|
|
25
|
+
* Grant with no live operation throws. Memory only: the owner's approval and frozen query/reply, resend
|
|
26
|
+
* timers, the consent time. Per-identifier {@link Unblind} owners come through a narrow interface (`runtime.ipex`). Group identifiers
|
|
27
|
+
* are refused; group Unblind belongs to the group proposal API.
|
|
28
|
+
*/
|
|
29
|
+
import { bytesEqual, collectPayload, framePayload, parseBytes, SerderKERI, Vrsn_2_0 } from "cesr-ts";
|
|
30
|
+
import { call, race } from "effection";
|
|
31
|
+
import { Unblind } from "../../acdc/unblinding.js";
|
|
32
|
+
import { ValidationError } from "../../core/errors.js";
|
|
33
|
+
import { singleKeyQueryWindow } from "../../core/mailbox/kram-policy.js";
|
|
34
|
+
import { timestampMicros } from "../../time/mod.js";
|
|
35
|
+
import { groupSigningMembers, isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
|
|
36
|
+
import { MULTISIG_QRY_ROUTE, MULTISIG_RPY_ROUTE, multisigExnBody } from "../multisig/grouping.js";
|
|
37
|
+
import { envelopeFromFrame } from "../protocol/parsering.js";
|
|
38
|
+
import { DeliveryOutbox } from "../runtime/delivery-outbox.js";
|
|
39
|
+
import { observeUntil, waitOnClock } from "../runtime/workflow-contract.js";
|
|
40
|
+
/** Milliseconds between resends of the unchanged disclosee query, as `tufa ipex unblind` does. */
|
|
41
|
+
export const UNBLIND_QUERY_RESEND_MS = 1000;
|
|
42
|
+
/** Per-operation state of a group party; memory only. */
|
|
43
|
+
class GroupUnblind {
|
|
44
|
+
constructor(member, proposer) {
|
|
45
|
+
Object.defineProperty(this, "member", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
writable: true,
|
|
49
|
+
value: member
|
|
50
|
+
});
|
|
51
|
+
Object.defineProperty(this, "proposer", {
|
|
52
|
+
enumerable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
writable: true,
|
|
55
|
+
value: proposer
|
|
56
|
+
});
|
|
57
|
+
Object.defineProperty(this, "pending", {
|
|
58
|
+
enumerable: true,
|
|
59
|
+
configurable: true,
|
|
60
|
+
writable: true,
|
|
61
|
+
value: new Map()
|
|
62
|
+
});
|
|
63
|
+
Object.defineProperty(this, "seen", {
|
|
64
|
+
enumerable: true,
|
|
65
|
+
configurable: true,
|
|
66
|
+
writable: true,
|
|
67
|
+
value: new Set()
|
|
68
|
+
});
|
|
69
|
+
/** The exact DH-style body this member contributes to, once frozen. */
|
|
70
|
+
Object.defineProperty(this, "frozen", {
|
|
71
|
+
enumerable: true,
|
|
72
|
+
configurable: true,
|
|
73
|
+
writable: true,
|
|
74
|
+
value: void 0
|
|
75
|
+
});
|
|
76
|
+
Object.defineProperty(this, "contributed", {
|
|
77
|
+
enumerable: true,
|
|
78
|
+
configurable: true,
|
|
79
|
+
writable: true,
|
|
80
|
+
value: false
|
|
81
|
+
});
|
|
82
|
+
Object.defineProperty(this, "finished", {
|
|
83
|
+
enumerable: true,
|
|
84
|
+
configurable: true,
|
|
85
|
+
writable: true,
|
|
86
|
+
value: false
|
|
87
|
+
});
|
|
88
|
+
Object.defineProperty(this, "started", {
|
|
89
|
+
enumerable: true,
|
|
90
|
+
configurable: true,
|
|
91
|
+
writable: true,
|
|
92
|
+
value: false
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const EXPIRED = "The presentation reached its original exchange deadline";
|
|
97
|
+
const CHANGED = "Original presentation deadline changed";
|
|
98
|
+
function message(error) {
|
|
99
|
+
return error instanceof Error ? error.message : String(error);
|
|
100
|
+
}
|
|
101
|
+
/** Whether the Grant's deadline has passed. */
|
|
102
|
+
function lapsed(clock, deadline) {
|
|
103
|
+
const expiry = timestampMicros(deadline);
|
|
104
|
+
return expiry !== null && BigInt(clock.now()) * 1000n >= expiry;
|
|
105
|
+
}
|
|
106
|
+
const INTERRUPTED = "No live Unblind holds this Grant: the process that began it ended (or it was never begun); nothing was signed since. Request it again";
|
|
107
|
+
/** The live operations table and its driver. One per host runtime; every identifier may unblind through it. */
|
|
108
|
+
export class UnblindingOperations {
|
|
109
|
+
constructor(deps) {
|
|
110
|
+
Object.defineProperty(this, "deps", {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
value: deps
|
|
115
|
+
});
|
|
116
|
+
/** Operations whose approval is live in this process. Internal to this module's functions. */
|
|
117
|
+
Object.defineProperty(this, "live", {
|
|
118
|
+
enumerable: true,
|
|
119
|
+
configurable: true,
|
|
120
|
+
writable: true,
|
|
121
|
+
value: new Map()
|
|
122
|
+
});
|
|
123
|
+
/** Operations that finished or ended in this process; not durable, see the module doc. */
|
|
124
|
+
Object.defineProperty(this, "settled", {
|
|
125
|
+
enumerable: true,
|
|
126
|
+
configurable: true,
|
|
127
|
+
writable: true,
|
|
128
|
+
value: new Map()
|
|
129
|
+
});
|
|
130
|
+
/** The Unblind child handlers registered on the multiplexor, once a group operation began. */
|
|
131
|
+
Object.defineProperty(this, "groupRoutes", {
|
|
132
|
+
enumerable: true,
|
|
133
|
+
configurable: true,
|
|
134
|
+
writable: true,
|
|
135
|
+
value: null
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Whether the live operation for `grant` still holds a message the transport has not taken. */
|
|
139
|
+
owes(grant) {
|
|
140
|
+
return (this.live.get(grant)?.outbox.length ?? 0) > 0;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Whether a pass has work: a live operation, or an owner holds an authenticated peer query whose reply is
|
|
144
|
+
* owed (an in-memory walk of the owners, no store read).
|
|
145
|
+
*/
|
|
146
|
+
active() {
|
|
147
|
+
if (this.live.size > 0)
|
|
148
|
+
return true;
|
|
149
|
+
for (const owner of this.deps.unblinds.unblindOwners())
|
|
150
|
+
if (owner.awaitingReply().length)
|
|
151
|
+
return true;
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
/** Advance every live operation by one pass, each bounded by its Grant deadline. */
|
|
155
|
+
*processOnce() {
|
|
156
|
+
const { deps } = this;
|
|
157
|
+
this.adopt();
|
|
158
|
+
for (const op of [...this.live.values()]) {
|
|
159
|
+
if (op.busy || !this.live.has(op.grant))
|
|
160
|
+
continue;
|
|
161
|
+
op.busy = true;
|
|
162
|
+
try {
|
|
163
|
+
const left = op.expires - BigInt(deps.clock.now()) * 1000n;
|
|
164
|
+
if (left <= 0n)
|
|
165
|
+
this.end(op, "expired", EXPIRED);
|
|
166
|
+
else if ((yield* race([this.advance(op), this.timer(Number(left) / 1000)])) === "expired") {
|
|
167
|
+
this.end(op, "expired", EXPIRED);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
this.end(op, "failed", message(error));
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
op.busy = false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Take on the replies the owners owe. The discloser has no consent step: an owner that authenticated a
|
|
180
|
+
* peer query for a live Grant its identifier sent holds the operation, and this delivers its reply
|
|
181
|
+
* under the Grant's deadline. Decided from the owners' state, so a restarted process re-answers a
|
|
182
|
+
* repeated query.
|
|
183
|
+
*/
|
|
184
|
+
adopt() {
|
|
185
|
+
const { deps } = this;
|
|
186
|
+
for (const owner of deps.unblinds.unblindOwners()) {
|
|
187
|
+
for (const { xid, grant } of owner.awaitingReply()) {
|
|
188
|
+
if (this.live.has(grant))
|
|
189
|
+
continue;
|
|
190
|
+
let input, deadline, expires;
|
|
191
|
+
try {
|
|
192
|
+
input = deps.exc.retainedInput(grant);
|
|
193
|
+
deadline = input && deps.exc.exchangeDeadline(input.serder);
|
|
194
|
+
expires = timestampMicros(deadline);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
// Unreadable stored state fails this reply alone; release the owner's approval so it is not retried.
|
|
198
|
+
owner.abandon(xid);
|
|
199
|
+
this.settled.set(grant, { role: "responder", state: "failed", reason: message(error) });
|
|
200
|
+
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!input || !deadline || expires === null || BigInt(deps.clock.now()) * 1000n >= expires)
|
|
204
|
+
continue;
|
|
205
|
+
this.settled.delete(grant);
|
|
206
|
+
this.live.set(grant, {
|
|
207
|
+
grant,
|
|
208
|
+
xid,
|
|
209
|
+
role: "responder",
|
|
210
|
+
deadline,
|
|
211
|
+
hab: owner.hab,
|
|
212
|
+
owner,
|
|
213
|
+
peer: String(input.serder.sad.ri),
|
|
214
|
+
expires,
|
|
215
|
+
queryAt: 0,
|
|
216
|
+
nextQuery: 0,
|
|
217
|
+
phase: "proving",
|
|
218
|
+
outbox: new DeliveryOutbox(deps.retry),
|
|
219
|
+
finished: false,
|
|
220
|
+
busy: false,
|
|
221
|
+
});
|
|
222
|
+
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** End a live operation: release the approval, remember why until the process ends, tell the sink. */
|
|
227
|
+
end(op, state, reason) {
|
|
228
|
+
this.release(op);
|
|
229
|
+
this.settled.set(op.grant, { role: op.role, state, reason });
|
|
230
|
+
this.deps.changes?.push({ kind: "unblinding", id: op.grant });
|
|
231
|
+
}
|
|
232
|
+
/** Drop the live entry and the owner's approval and frozen messages; delivered bytes stay the peer's. */
|
|
233
|
+
release(op) {
|
|
234
|
+
this.live.delete(op.grant);
|
|
235
|
+
try {
|
|
236
|
+
op.owner.abandon(op.xid);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (!(error instanceof ValidationError))
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
finished(op) {
|
|
244
|
+
this.release(op);
|
|
245
|
+
this.settled.set(op.grant, { role: op.role, state: "done" });
|
|
246
|
+
this.deps.changes?.push({ kind: "unblinding", id: op.grant });
|
|
247
|
+
}
|
|
248
|
+
*timer(milliseconds) {
|
|
249
|
+
yield* waitOnClock(this.deps.clock, milliseconds);
|
|
250
|
+
return "expired";
|
|
251
|
+
}
|
|
252
|
+
setPhase(op, phase) {
|
|
253
|
+
if (op.phase === phase)
|
|
254
|
+
return;
|
|
255
|
+
op.phase = phase;
|
|
256
|
+
this.deps.changes?.push({ kind: "unblinding", id: op.grant });
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* One pass of a group party, in the order of the CLI loop it replaces: take in the members' wrappers, contribute
|
|
260
|
+
* this member's signature once to the frozen body, publish it to the other members, and finish.
|
|
261
|
+
*/
|
|
262
|
+
*stepGroup(op) {
|
|
263
|
+
const { deps } = this, { owner, xid, grant } = op, g = op.group, { member, proposer } = g;
|
|
264
|
+
const input = deps.exc.retainedInput(grant);
|
|
265
|
+
if (!input || deps.exc.exchangeDeadline(input.serder) !== op.deadline)
|
|
266
|
+
throw new ValidationError(CHANGED);
|
|
267
|
+
owner.complete(xid);
|
|
268
|
+
const requester = op.role === "requester", group = deps.group;
|
|
269
|
+
const send = function* (dest, msg, topic = "credential") {
|
|
270
|
+
op.outbox.enqueue({ src: member.pre, dest, msg, topic });
|
|
271
|
+
};
|
|
272
|
+
if (!g.started) {
|
|
273
|
+
g.started = true;
|
|
274
|
+
// Accepted wrappers can precede the consent; intake alone never contributed.
|
|
275
|
+
for (const [, parent] of deps.hby.db.exns.getTopItemIter()) {
|
|
276
|
+
const retained = parent.route === groupRoute(op) ? deps.exc.retainedInput(parent.said) : null;
|
|
277
|
+
if (retained?.nests && structural(deps.hby, parent, retained.nests)
|
|
278
|
+
&& groupAccepts(deps.hby, op, parent, retained.nests)) {
|
|
279
|
+
g.pending.set(parent.said, { parent, child: retained.nests[0] });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (!requester && member.pre === proposer && !g.contributed && owner.hasPeerQuery(xid))
|
|
284
|
+
owner.respond(xid);
|
|
285
|
+
const broadcast = function* (raw) {
|
|
286
|
+
const child = parseBytes(raw)[0];
|
|
287
|
+
if (!(child?.body instanceof SerderKERI) || child.body.ilk !== (requester ? "qry" : "rpy")) {
|
|
288
|
+
throw new ValidationError("Unexpected local Unblind proposal");
|
|
289
|
+
}
|
|
290
|
+
g.frozen = child.body.raw;
|
|
291
|
+
g.contributed = true;
|
|
292
|
+
yield* group.protocol.dispatch(child, { local: true });
|
|
293
|
+
const route = groupRoute(op);
|
|
294
|
+
const body = multisigExnBody(member.pre, { route, attrs: { gid: op.hab.pre } }, child.body, {
|
|
295
|
+
version: Vrsn_2_0,
|
|
296
|
+
});
|
|
297
|
+
const parent = parseBytes(member.endorse(body, { gvrsn: Vrsn_2_0 }))[0];
|
|
298
|
+
parent.nests = [child];
|
|
299
|
+
const proposal = yield* call(() => collectPayload(framePayload(parent)));
|
|
300
|
+
for (const peer of groupSigningMembers(deps.hby, op.hab.pre)) {
|
|
301
|
+
if (peer !== member.pre)
|
|
302
|
+
yield* send(peer, proposal, "multisig");
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
for (const [said, item] of [...g.pending]) {
|
|
306
|
+
if (g.seen.has(said) || item.parent.pre === member.pre) {
|
|
307
|
+
g.pending.delete(said);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if ((!g.frozen && item.parent.pre !== proposer) || (!requester && !owner.hasPeerQuery(xid)))
|
|
311
|
+
continue;
|
|
312
|
+
if (!groupAccepts(deps.hby, op, item.parent, [item.child])) {
|
|
313
|
+
g.pending.delete(said);
|
|
314
|
+
g.seen.add(said);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const contribution = owner.contribute(xid, yield* call(() => collectPayload(framePayload(item.child))));
|
|
318
|
+
yield* group.protocol.dispatch(item.child, { local: true });
|
|
319
|
+
if (!g.contributed)
|
|
320
|
+
yield* broadcast(contribution);
|
|
321
|
+
g.seen.add(said);
|
|
322
|
+
g.pending.delete(said);
|
|
323
|
+
}
|
|
324
|
+
while (owner.outgoing.length) {
|
|
325
|
+
const message = owner.outgoing.pull();
|
|
326
|
+
if (message.kin === "sign") {
|
|
327
|
+
if (!g.contributed)
|
|
328
|
+
yield* broadcast(message.msg);
|
|
329
|
+
}
|
|
330
|
+
else if (message.kin === "query")
|
|
331
|
+
op.query = message.msg;
|
|
332
|
+
else if (!g.finished) {
|
|
333
|
+
yield* owner.finish(xid); // Validate exact evidence; no issuer-state admission policy here.
|
|
334
|
+
g.finished = true;
|
|
335
|
+
yield* send(message.dest, message.msg);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (requester && owner.complete(xid)) {
|
|
339
|
+
this.setPhase(op, "proving");
|
|
340
|
+
const decision = yield* owner.finish(xid);
|
|
341
|
+
if (decision.kind !== "accept")
|
|
342
|
+
throw new ValidationError("Updated presentation proofs did not verify");
|
|
343
|
+
return this.finishedStep(op);
|
|
344
|
+
}
|
|
345
|
+
if (!requester && (g.finished || owner.complete(xid))) {
|
|
346
|
+
if (!g.finished)
|
|
347
|
+
yield* owner.finish(xid);
|
|
348
|
+
return this.finishedStep(op);
|
|
349
|
+
}
|
|
350
|
+
// A threshold query cannot be re-dated by one member: it is resent unchanged.
|
|
351
|
+
if (op.query && deps.clock.now() >= op.nextQuery) {
|
|
352
|
+
yield* send(op.peer, op.query);
|
|
353
|
+
op.nextQuery = deps.clock.now() + UNBLIND_QUERY_RESEND_MS;
|
|
354
|
+
}
|
|
355
|
+
return "stepped";
|
|
356
|
+
}
|
|
357
|
+
/** One pass, in the order of the CLI loop it replaces. */
|
|
358
|
+
*step(op) {
|
|
359
|
+
if (op.group)
|
|
360
|
+
return yield* this.stepGroup(op);
|
|
361
|
+
const { deps } = this, { owner, xid, grant } = op;
|
|
362
|
+
const send = function* (dest, msg) {
|
|
363
|
+
op.outbox.enqueue({ src: op.hab.pre, dest, msg });
|
|
364
|
+
};
|
|
365
|
+
const input = deps.exc.retainedInput(grant);
|
|
366
|
+
if (!input || deps.exc.exchangeDeadline(input.serder) !== op.deadline)
|
|
367
|
+
throw new ValidationError(CHANGED);
|
|
368
|
+
owner.complete(xid); // Rechecks exact live scope and accepted current state; readiness is separate.
|
|
369
|
+
let replied = false;
|
|
370
|
+
while (owner.outgoing.length) {
|
|
371
|
+
const queued = owner.outgoing.pull();
|
|
372
|
+
if (queued.kin === "sign")
|
|
373
|
+
throw new ValidationError("Individual Unblind cannot process group contributions");
|
|
374
|
+
if (queued.kin === "query")
|
|
375
|
+
op.query = queued.msg;
|
|
376
|
+
else {
|
|
377
|
+
yield* owner.finish(xid); // Validate exact evidence; no issuer-state admission policy here.
|
|
378
|
+
yield* send(queued.dest, queued.msg);
|
|
379
|
+
replied = true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (replied)
|
|
383
|
+
return this.finishedStep(op);
|
|
384
|
+
if (op.role === "requester" && owner.complete(xid)) {
|
|
385
|
+
this.setPhase(op, "proving");
|
|
386
|
+
const decision = yield* owner.finish(xid);
|
|
387
|
+
if (decision.kind !== "accept")
|
|
388
|
+
throw new ValidationError("Updated presentation proofs did not verify");
|
|
389
|
+
return this.finishedStep(op);
|
|
390
|
+
}
|
|
391
|
+
if (op.query && deps.clock.now() >= op.nextQuery) {
|
|
392
|
+
yield* send(op.peer, () => this.current(op));
|
|
393
|
+
op.nextQuery = deps.clock.now() + UNBLIND_QUERY_RESEND_MS;
|
|
394
|
+
}
|
|
395
|
+
return "stepped";
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The query to send now: the signed bytes unchanged while they are timely, and a re-dated one
|
|
399
|
+
* (`Unblind.requery`: same frozen request, new `dt`, SAID and signatures) once they are older than the
|
|
400
|
+
* single-key window, which the discloser's KRAM would drop as stale. Re-signing happens only here, inside
|
|
401
|
+
* the consent that began the operation; a registry head that moved since fails the operation instead. A
|
|
402
|
+
* group threshold query cannot be re-dated by one member; group Unblind is not run here.
|
|
403
|
+
*/
|
|
404
|
+
current(op) {
|
|
405
|
+
const now = this.deps.clock.now(), window = singleKeyQueryWindow(this.deps.hby.db, Unblind.Route);
|
|
406
|
+
if (window !== null && now - op.queryAt >= window) {
|
|
407
|
+
op.query = op.owner.requery(op.xid);
|
|
408
|
+
op.queryAt = now;
|
|
409
|
+
}
|
|
410
|
+
return op.query;
|
|
411
|
+
}
|
|
412
|
+
finishedStep(op) {
|
|
413
|
+
op.finished = true;
|
|
414
|
+
return "stepped";
|
|
415
|
+
}
|
|
416
|
+
/** One pass: send what is queued, step only once the queue is empty, send what the step queued. */
|
|
417
|
+
*advance(op) {
|
|
418
|
+
const drain = () => op.outbox.drain(this.deps.delivery, this.deps.clock);
|
|
419
|
+
if (!(yield* drain()))
|
|
420
|
+
return "stepped";
|
|
421
|
+
if (!op.finished)
|
|
422
|
+
yield* this.step(op);
|
|
423
|
+
if ((yield* drain()) && op.finished)
|
|
424
|
+
this.finished(op);
|
|
425
|
+
return "stepped";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/** The exact live Grant the local party may consent to, with its deadline and role. */
|
|
429
|
+
function selectGrant(ops, grant, role, proposer) {
|
|
430
|
+
const { deps } = ops;
|
|
431
|
+
if (ops.live.has(grant))
|
|
432
|
+
throw new ValidationError("Unblind was already begun for this Grant");
|
|
433
|
+
const input = deps.exc.retainedInput(grant);
|
|
434
|
+
if (!input)
|
|
435
|
+
throw new ValidationError("Unblind requires an authenticated retained Grant");
|
|
436
|
+
const sender = input.serder.pre, recipient = input.serder.sad?.ri;
|
|
437
|
+
const party = role === "requester" ? recipient : sender;
|
|
438
|
+
const hab = typeof party === "string" ? deps.hby.habs.get(party) : undefined;
|
|
439
|
+
if (!hab)
|
|
440
|
+
throw new ValidationError("Local habitat is not a presentation party");
|
|
441
|
+
if (isLocalGroupHab(deps.hby, hab)) {
|
|
442
|
+
if (!deps.group)
|
|
443
|
+
throw new ValidationError("This Unblind owner was composed without group collaborators");
|
|
444
|
+
if (!proposer || !groupSigningMembers(deps.hby, hab.pre).includes(proposer)) {
|
|
445
|
+
throw new ValidationError("Group Unblind requires a current signing member proposer");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else if (proposer !== undefined) {
|
|
449
|
+
throw new ValidationError("Individual Unblind has no proposer");
|
|
450
|
+
}
|
|
451
|
+
const deadline = deps.exc.exchangeDeadline(input.serder), expires = timestampMicros(deadline);
|
|
452
|
+
if (!deadline || expires === null)
|
|
453
|
+
throw new ValidationError("Missing or corrupt presentation deadline");
|
|
454
|
+
if (BigInt(deps.clock.now()) * 1000n >= expires)
|
|
455
|
+
throw new ValidationError("Original presentation deadline expired");
|
|
456
|
+
return { hab, input, deadline, expires, peer: sender };
|
|
457
|
+
}
|
|
458
|
+
function consent(ops, grant, role, options) {
|
|
459
|
+
const { deps } = ops, selected = selectGrant(ops, grant, role, options.proposer), { hab } = selected;
|
|
460
|
+
if (role === "responder" && !isLocalGroupHab(deps.hby, hab)) {
|
|
461
|
+
throw new ValidationError("An individual discloser answers an authenticated query without consent");
|
|
462
|
+
}
|
|
463
|
+
const owner = deps.unblinds.unblind(hab), xid = owner.approve(grant, { states: options.states });
|
|
464
|
+
const begun = { grant, xid, role };
|
|
465
|
+
// A current authenticated view needs no empty auxiliary query or new proof writes.
|
|
466
|
+
if (role === "requester" && deps.exc.complete(grant)) {
|
|
467
|
+
const current = deps.exc.verifyGrantProofs(grant, selected.input.nests ?? []);
|
|
468
|
+
if (current === true || typeof current === "object" && current.kind === "accept") {
|
|
469
|
+
owner.abandon(xid);
|
|
470
|
+
ops.settled.set(grant, { role: "requester", state: "done" });
|
|
471
|
+
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
472
|
+
return { op: null, begun };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
begun,
|
|
477
|
+
op: {
|
|
478
|
+
grant,
|
|
479
|
+
xid,
|
|
480
|
+
role,
|
|
481
|
+
deadline: selected.deadline,
|
|
482
|
+
hab,
|
|
483
|
+
owner,
|
|
484
|
+
peer: selected.peer,
|
|
485
|
+
expires: selected.expires,
|
|
486
|
+
queryAt: deps.clock.now(),
|
|
487
|
+
nextQuery: 0,
|
|
488
|
+
group: options.proposer ? new GroupUnblind(localGroupMember(deps.hby, hab.pre), options.proposer) : undefined,
|
|
489
|
+
phase: role === "requester" ? "awaitingReply" : "proving",
|
|
490
|
+
outbox: new DeliveryOutbox(deps.retry),
|
|
491
|
+
finished: false,
|
|
492
|
+
busy: false,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* The disclosee's consent to request the missing proofs for one live Grant: approves the Grant, signs the
|
|
498
|
+
* query over the missing accepted public suffix and records it. Returns after local commit; `processOnce`
|
|
499
|
+
* sends and resends it. A Grant whose proofs are already current reads `done` at once.
|
|
500
|
+
*
|
|
501
|
+
* @throws ValidationError for a Grant that is not live and addressed to a local identifier, a group without a
|
|
502
|
+
* current signing member as proposer, or an Unblind already begun for it.
|
|
503
|
+
*/
|
|
504
|
+
export function requestUnblind(ops, grant, options = {}) {
|
|
505
|
+
const { op, begun } = consent(ops, grant, "requester", options);
|
|
506
|
+
if (op) {
|
|
507
|
+
if (op.group) {
|
|
508
|
+
registerUnblindRoutes(ops);
|
|
509
|
+
// Only the proposer authors the threshold query; the other members contribute to it when it arrives.
|
|
510
|
+
if (op.group.member.pre === options.proposer)
|
|
511
|
+
op.owner.start(grant);
|
|
512
|
+
}
|
|
513
|
+
else
|
|
514
|
+
op.query = op.owner.start(grant);
|
|
515
|
+
ops.settled.delete(grant);
|
|
516
|
+
ops.live.set(grant, op);
|
|
517
|
+
ops.deps.changes?.push({ kind: "unblinding", id: grant });
|
|
518
|
+
}
|
|
519
|
+
return begun;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* A group discloser member's consent to answer a live presentation: approves the Grant, so that this member
|
|
523
|
+
* contributes its signature to the one threshold reply (the proposer authors the reply once the disclosee's
|
|
524
|
+
* authenticated query arrives; the others contribute to it). Returns after local commit; `processOnce` does
|
|
525
|
+
* the rest. An individual discloser needs no consent: its owner answers an authenticated query by itself.
|
|
526
|
+
*
|
|
527
|
+
* @throws ValidationError for an individual identifier, a Grant the group did not send, a proposer that is not a
|
|
528
|
+
* current signing member, or an Unblind already begun.
|
|
529
|
+
*/
|
|
530
|
+
export function answerUnblind(ops, grant, options = {}) {
|
|
531
|
+
const { deps } = ops;
|
|
532
|
+
const { op, begun } = consent(ops, grant, "responder", options);
|
|
533
|
+
registerUnblindRoutes(ops);
|
|
534
|
+
ops.settled.delete(grant);
|
|
535
|
+
ops.live.set(grant, op);
|
|
536
|
+
deps.changes?.push({ kind: "unblinding", id: grant });
|
|
537
|
+
return begun;
|
|
538
|
+
}
|
|
539
|
+
/** The wrapper route an operation's party uses: the disclosee's query, the discloser's reply. */
|
|
540
|
+
function groupRoute(op) {
|
|
541
|
+
return op.role === "requester" ? MULTISIG_QRY_ROUTE : MULTISIG_RPY_ROUTE;
|
|
542
|
+
}
|
|
543
|
+
/** The structure every Unblind child wrapper has, whichever operation it is for. */
|
|
544
|
+
function structural(hby, parent, children) {
|
|
545
|
+
const child = children[0], body = child?.body, route = parent.route;
|
|
546
|
+
if (children.length !== 1 || !(body instanceof SerderKERI) || parent.pvrsn.major !== 2 || parent.pvrsn.minor !== 0
|
|
547
|
+
|| (route !== MULTISIG_QRY_ROUTE && route !== MULTISIG_RPY_ROUTE))
|
|
548
|
+
return false;
|
|
549
|
+
const requester = route === MULTISIG_QRY_ROUTE, field = requester ? "qry" : "rpy", gid = body.pre;
|
|
550
|
+
const attrs = parent.sad.a;
|
|
551
|
+
if (!gid || !hby.habs.has(gid) || !groupSigningMembers(hby, gid).includes(parent.pre) || !attrs
|
|
552
|
+
|| Object.keys(attrs).length !== 2 || attrs.gid !== gid || attrs[field] !== body.said)
|
|
553
|
+
return false;
|
|
554
|
+
const data = body.sad[requester ? "q" : "a"];
|
|
555
|
+
if (body.ilk !== field || body.route !== Unblind.Route || body.pvrsn.major !== 2 || body.pvrsn.minor !== 0
|
|
556
|
+
|| typeof data?.x !== "string" || (requester && body.sad.rr !== Unblind.Route))
|
|
557
|
+
return false;
|
|
558
|
+
const allowed = requester
|
|
559
|
+
? ["TransIdxSigGroups", "TransLastIdxSigGroups"]
|
|
560
|
+
: ["TransIdxSigGroups", "BlindedStateQuadruples", "BoundStateSextuples"];
|
|
561
|
+
return (requester || !child.nests?.length)
|
|
562
|
+
&& envelopeFromFrame(child).attachmentGroups.every((group) => allowed.includes(group.name.replace(/^Big/, "")));
|
|
563
|
+
}
|
|
564
|
+
/** Operation-specific acceptance: the group's own transaction, in the direction this party consumes. */
|
|
565
|
+
function groupAccepts(hby, op, parent, children) {
|
|
566
|
+
if (parent.route !== groupRoute(op) || !structural(hby, parent, children))
|
|
567
|
+
return false;
|
|
568
|
+
const body = children[0].body, requester = op.role === "requester";
|
|
569
|
+
const data = body.sad[requester ? "q" : "a"];
|
|
570
|
+
return body.pre === op.hab.pre && data?.x === op.xid && (!op.group?.frozen || bytesEqual(op.group.frozen, body.raw));
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Delegate the Unblind child wrappers of `/multisig/qry` and `/multisig/rpy` to this owner, once. The delegation
|
|
574
|
+
* is structural; operation-specific checks run at intake, and stored wrappers are found when an operation starts.
|
|
575
|
+
*/
|
|
576
|
+
function registerUnblindRoutes(ops) {
|
|
577
|
+
if (ops.groupRoutes)
|
|
578
|
+
return;
|
|
579
|
+
const { deps } = ops, mux = deps.group.mux;
|
|
580
|
+
ops.groupRoutes = [MULTISIG_QRY_ROUTE, MULTISIG_RPY_ROUTE].map((route) => [route, {
|
|
581
|
+
resource: route,
|
|
582
|
+
acceptsNests: true,
|
|
583
|
+
verify: (args) => structural(deps.hby, args.serder, args.nests ?? []) && !args.attachments.length && !args.essrs?.length,
|
|
584
|
+
handle: (args) => {
|
|
585
|
+
for (const op of ops.live.values()) {
|
|
586
|
+
if (op.group && groupAccepts(deps.hby, op, args.serder, args.nests)) {
|
|
587
|
+
op.group.pending.set(args.serder.said, { parent: args.serder, child: args.nests[0] });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
}]);
|
|
592
|
+
for (const [route, handler] of ops.groupRoutes)
|
|
593
|
+
mux.addChildHandler(route, Unblind.Route, handler);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Pure read of one Unblind: what finished or ended in this process, the live table, and otherwise what the
|
|
597
|
+
* stores prove for the requester (see the module doc for what a reload loses). A requester's Grant whose
|
|
598
|
+
* proofs are not current and that no live operation holds reads `interrupted`, or `expired` past the Grant
|
|
599
|
+
* deadline; that is a statement, not a write.
|
|
600
|
+
*
|
|
601
|
+
* @throws ValidationError when `grant` is not a Grant a local identifier is the disclosee of, and no
|
|
602
|
+
* operation for it is live or remembered.
|
|
603
|
+
*/
|
|
604
|
+
export function unblindingStatus(ops, grant) {
|
|
605
|
+
const { deps } = ops, settled = ops.settled.get(grant), live = ops.live.get(grant);
|
|
606
|
+
if (settled) {
|
|
607
|
+
return settled.state === "done"
|
|
608
|
+
? { state: "done", role: settled.role }
|
|
609
|
+
: { state: settled.state, role: settled.role, reason: settled.reason };
|
|
610
|
+
}
|
|
611
|
+
if (live && !lapsed(deps.clock, live.deadline)) {
|
|
612
|
+
return {
|
|
613
|
+
state: "active",
|
|
614
|
+
role: live.role,
|
|
615
|
+
phase: live.phase,
|
|
616
|
+
deadline: live.deadline,
|
|
617
|
+
...(live.outbox.failure && { failure: live.outbox.failure }),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const input = deps.exc.retainedInput(grant), recipient = input?.serder.sad?.ri;
|
|
621
|
+
if (!input || typeof recipient !== "string" || !deps.hby.habs.has(recipient)) {
|
|
622
|
+
throw new ValidationError("No Unblind was begun for this Grant");
|
|
623
|
+
}
|
|
624
|
+
if (deps.exc.complete(grant)) {
|
|
625
|
+
const current = deps.exc.verifyGrantProofs(grant, input.nests ?? []);
|
|
626
|
+
if (current === true || typeof current === "object" && current.kind === "accept") {
|
|
627
|
+
return { state: "done", role: "requester" };
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const deadline = deps.exc.exchangeDeadline(input.serder);
|
|
631
|
+
if (!deadline)
|
|
632
|
+
throw new ValidationError("No Unblind was begun for this Grant");
|
|
633
|
+
if (lapsed(deps.clock, deadline))
|
|
634
|
+
return { state: "expired", role: "requester", reason: EXPIRED };
|
|
635
|
+
return { state: "interrupted", role: "requester", reason: INTERRUPTED };
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Observe until the Unblind is done or ended, or the budget expires. Starts no worker and drives nothing:
|
|
639
|
+
* someone else must call `processOnce`.
|
|
640
|
+
*/
|
|
641
|
+
export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clock }) {
|
|
642
|
+
const settled = yield* observeUntil(() => unblindingStatus(ops, grant).state !== "active" && !ops.owes(grant), {
|
|
643
|
+
timeoutMs,
|
|
644
|
+
clock,
|
|
645
|
+
});
|
|
646
|
+
const status = unblindingStatus(ops, grant);
|
|
647
|
+
return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Local abandon: release the approval and frozen messages and read `abandoned` until the process ends. Does
|
|
651
|
+
* not message the peer and does not undo proofs already finished. Does nothing to an Unblind that is already
|
|
652
|
+
* done or ended.
|
|
653
|
+
*
|
|
654
|
+
* @throws ValidationError when no Unblind was begun for `grant`.
|
|
655
|
+
*/
|
|
656
|
+
export function abandonUnblinding(ops, grant, reason = "Abandoned by the user") {
|
|
657
|
+
const status = unblindingStatus(ops, grant);
|
|
658
|
+
if (status.state !== "active" && status.state !== "interrupted")
|
|
659
|
+
return;
|
|
660
|
+
const live = ops.live.get(grant);
|
|
661
|
+
if (live)
|
|
662
|
+
ops.end(live, "abandoned", reason);
|
|
663
|
+
else {
|
|
664
|
+
ops.settled.set(grant, { role: status.role, state: "abandoned", reason });
|
|
665
|
+
ops.deps.changes?.push({ kind: "unblinding", id: grant });
|
|
666
|
+
}
|
|
667
|
+
}
|