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,896 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-owned operations for single-signature v2 DH issuance.
|
|
3
|
+
*
|
|
4
|
+
* One `IssuanceOperations` owner holds an in-memory table of live operations. The user begins an
|
|
5
|
+
* operation with `beginIssuance`, which signs the Grant, approves the DH exchange, starts the query
|
|
6
|
+
* and records that authorisation in memory. The host's runtime loop (or, in a one-shot CLI, a driver
|
|
7
|
+
* under `withRuntimeWorkers`) then calls `processOnce`, which performs exactly the steps
|
|
8
|
+
* `tufa ipex --issue` performed itself: send the proof support and Grant, deliver DH traffic, resend
|
|
9
|
+
* a freshly dated query each second until DH completes, and once DH completes stage and anchor the registry update (BUP),
|
|
10
|
+
* `finish` the Grant and send the final proof support. Nothing starts an operation or signs except
|
|
11
|
+
* inside an operation the user began while its in-memory authorisation lives.
|
|
12
|
+
*
|
|
13
|
+
* Workflow-contract shape (`app/runtime/workflow-contract.ts`): `beginIssuance` returns after local
|
|
14
|
+
* commit and never waits for the peer; `issuanceStatus` is a pure read; `waitForIssuance` only
|
|
15
|
+
* observes and returns a `WaitResult`; `abandonIssuance` is local.
|
|
16
|
+
*
|
|
17
|
+
* There is no operation store. Status is derived from the protocol stores plus the live table, so a
|
|
18
|
+
* reopened store reads what the stores prove, whatever the process was doing:
|
|
19
|
+
*
|
|
20
|
+
* | stores prove | status |
|
|
21
|
+
* | ------------------------------------------------------------------------- | ---------------------------------------- |
|
|
22
|
+
* | holder: pending Grant, no root held | `awaitingExchangeConsent` |
|
|
23
|
+
* | pending Grant past its deadline | `expired` |
|
|
24
|
+
* | holder: root held, not committed otherwise; issuer: update accepted | `active`, `rootRetained` (adopted) |
|
|
25
|
+
* | issuer: pending Grant, update not accepted | `interrupted` |
|
|
26
|
+
* | root held and the registry committed to another exchange | `interrupted` |
|
|
27
|
+
* | holder: Grant accepted, no accepted Admit | `active`, `awaitingAdmitConsent` |
|
|
28
|
+
* | holder: accepted Admit | `done`, `admitted` |
|
|
29
|
+
* | issuer: Grant accepted | `done`, `issued` |
|
|
30
|
+
*
|
|
31
|
+
* Lost on reload, by design: the phases that live only in memory (`exchangingKeys`, `awaitingAnchor`,
|
|
32
|
+
* `proving` read `rootRetained` or `interrupted` afterwards), the `abandoned` and `failed` end states
|
|
33
|
+
* with their reasons (an abandoned holder reads `awaitingExchangeConsent` again, an abandoned issuer
|
|
34
|
+
* `interrupted`, and an abandoned operation whose root and accepted update remain is adopted and
|
|
35
|
+
* completed on the next start; it signs nothing and Admit stays a separate consent), the consent
|
|
36
|
+
* timestamps, and whether the issuer's final proof support was delivered (an issuer whose Grant is
|
|
37
|
+
* accepted reads `done` even if the process ended before that message went out). A Grant the issuer
|
|
38
|
+
* prepared but never began reads `interrupted`, the same as one whose process ended; so does an issuer
|
|
39
|
+
* that ended between staging its registry update and anchoring it (an explicit recovery action for that
|
|
40
|
+
* case is a planned follow-up; until then it needs a new Grant, and a held root nothing committed to is
|
|
41
|
+
* replaced). A wallet that wants any of these keeps them itself from the `ChangeSink`.
|
|
42
|
+
*
|
|
43
|
+
* After a restart the first `processOnce` finds the pending Grants this runtime's identifiers are party
|
|
44
|
+
* to that hold a root and can finish without new key exchange ({@link resumableExchange}) and adopts
|
|
45
|
+
* them: the holder waits for or verifies the issuer's update, an issuer whose update is already
|
|
46
|
+
* accepted finishes. Neither signs: the issuer's update was anchored before the restart, and
|
|
47
|
+
* `Dhke.finish` only proves the held root against the accepted update before the Grant is accepted. A
|
|
48
|
+
* new Grant on a registry that holds an unused root (no update staged or accepted) replaces that root
|
|
49
|
+
* when its exchange retains its own; otherwise the refusal stands. A KERIpy peer keeps the old rule and
|
|
50
|
+
* refuses a second exchange on a registry it holds a root for. The Grant's exchange deadline bounds
|
|
51
|
+
* everything, even a late completion; passing it reads `expired`.
|
|
52
|
+
*
|
|
53
|
+
* The runtime never writes wallet inventory. Completion is pushed to the `ChangeSink` as
|
|
54
|
+
* `{kind: "issuance", id: <grant>}` and the embedder records it.
|
|
55
|
+
*
|
|
56
|
+
* A local group issues and receives through the same table, status and wait: its consent calls and per-operation
|
|
57
|
+
* steps are in `group-issuance.ts` and run inside the same `processOnce`. A group exchange is not resumed after a
|
|
58
|
+
* restart (its frozen coordinator and custodian are not stored): a group pending Grant with no root reads
|
|
59
|
+
* `awaitingExchangeConsent`, and a member consents again with the same call.
|
|
60
|
+
*
|
|
61
|
+
* Collaborators are narrow concrete owners (ADR-0013); no `AgentRuntime`.
|
|
62
|
+
*/
|
|
63
|
+
import { collectPayload, concatBytes, Counter, framePayload, parseBytes, SerderACDC, Vrsn_2_0, } from "cesr-ts";
|
|
64
|
+
import { call, race } from "effection";
|
|
65
|
+
import { RegistryAuthor } from "../../acdc/authoring.js";
|
|
66
|
+
import { RegistryCustody } from "../../acdc/custody.js";
|
|
67
|
+
import { grantTransactionId, registryUncommitted, resumableExchange } from "../../acdc/dhking.js";
|
|
68
|
+
import { ValidationError } from "../../core/errors.js";
|
|
69
|
+
import { CREDENTIAL_MAILBOX_TOPIC } from "../../core/mailbox/mailbox-topics.js";
|
|
70
|
+
import { timestampMicros } from "../../time/mod.js";
|
|
71
|
+
import { isLocalGroupHab } from "../multisig/group-members.js";
|
|
72
|
+
import { DeliveryOutbox, } from "../runtime/delivery-outbox.js";
|
|
73
|
+
import { observeUntil, waitOnClock } from "../runtime/workflow-contract.js";
|
|
74
|
+
import { deriveGroupCustody, GroupIssuance, GroupParty, registerGroupRoutes, stepGroup, } from "./group-issuance.js";
|
|
75
|
+
import { IpexAdmitter } from "./ipex-admitting.js";
|
|
76
|
+
import { exchangeWire, IpexDiscloser, storedExchangeWire } from "./ipex-discloser.js";
|
|
77
|
+
/**
|
|
78
|
+
* Milliseconds between resends of the issuer query. Each resend is a new query (new date, SAID and signatures):
|
|
79
|
+
* the peer's KRAM caches every query it accepts, so an identical resend is a replay it drops, which would
|
|
80
|
+
* strand a peer that took the first query and then lost it before consenting (a process that ended, or a
|
|
81
|
+
* retained query that outlived its window).
|
|
82
|
+
*/
|
|
83
|
+
export const ISSUANCE_QUERY_RESEND_MS = 1000;
|
|
84
|
+
/**
|
|
85
|
+
* Milliseconds between scans of the pending exchange escrow for Grants a restarted process can finish. The scan
|
|
86
|
+
* repeats because what makes a Grant resumable can arrive after the restart (an issuer's update accepted once the
|
|
87
|
+
* other members approve it).
|
|
88
|
+
*/
|
|
89
|
+
export const ISSUANCE_SWEEP_MS = 5_000;
|
|
90
|
+
/**
|
|
91
|
+
* Adapt a `Poster`; its delivery worker must be running (a one-shot CLI runs it under `withRuntimeWorkers`).
|
|
92
|
+
* A wire of several complete messages (proof support plus Grant) goes as one ordered stream, which mailbox
|
|
93
|
+
* forwarding needs; a single message goes as an ordinary send, which is what a direct peer's query route expects.
|
|
94
|
+
*/
|
|
95
|
+
export function posterDelivery(postman) {
|
|
96
|
+
return {
|
|
97
|
+
*deliver({ src, dest, msg, topic = CREDENTIAL_MAILBOX_TOPIC }) {
|
|
98
|
+
const event = { src, dest, topic };
|
|
99
|
+
const sent = parseBytes(msg).length > 1
|
|
100
|
+
? postman.sendStream({ ...event, msgs: [msg] }, { notify: true })
|
|
101
|
+
: postman.send({ ...event, msg }, { notify: true });
|
|
102
|
+
const outcome = yield* postman.wait(sent);
|
|
103
|
+
if (outcome.kin === "failed")
|
|
104
|
+
throw outcome.error;
|
|
105
|
+
return outcome.deliveries;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** How long delivery failures may last on an operation that has no Grant deadline yet before it ends `failed`. */
|
|
110
|
+
export const ISSUANCE_RETRY_BOUND_MS = 600_000;
|
|
111
|
+
const EXPIRED = "The selected Grant reached its original exchange deadline";
|
|
112
|
+
const EXPIRED_LATE = "The selected Grant reached its original exchange deadline after the issuer's registry update arrived; a Grant is completed only while live, so ask the issuer for a new Grant";
|
|
113
|
+
const RESTARTED = "No live issuance holds this Grant: the process that began it ended before the registry update was anchored (or it was never begun); nothing was signed since. Begin the Grant, or a new one: its key exchange replaces any unused root";
|
|
114
|
+
const COMMITTED = "The process that ended a key exchange left a staged or accepted registry update that this Grant can no longer complete, and the registry cannot take another key exchange";
|
|
115
|
+
function message(error) {
|
|
116
|
+
return error instanceof Error ? error.message : String(error);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The live operations table and its driver. One per host runtime; every identifier of that runtime may
|
|
120
|
+
* issue through it. Only `processOnce` performs work, and only for operations in
|
|
121
|
+
* {@link IssuanceOperations.live}.
|
|
122
|
+
*/
|
|
123
|
+
export class IssuanceOperations {
|
|
124
|
+
constructor(deps) {
|
|
125
|
+
Object.defineProperty(this, "deps", {
|
|
126
|
+
enumerable: true,
|
|
127
|
+
configurable: true,
|
|
128
|
+
writable: true,
|
|
129
|
+
value: deps
|
|
130
|
+
});
|
|
131
|
+
/** Operations live in this process. Internal to this module's functions. */
|
|
132
|
+
Object.defineProperty(this, "live", {
|
|
133
|
+
enumerable: true,
|
|
134
|
+
configurable: true,
|
|
135
|
+
writable: true,
|
|
136
|
+
value: new Map()
|
|
137
|
+
});
|
|
138
|
+
/** Operations that ended in this process with their reasons; not durable, see the module doc. */
|
|
139
|
+
Object.defineProperty(this, "ended", {
|
|
140
|
+
enumerable: true,
|
|
141
|
+
configurable: true,
|
|
142
|
+
writable: true,
|
|
143
|
+
value: new Map()
|
|
144
|
+
});
|
|
145
|
+
Object.defineProperty(this, "discloser", {
|
|
146
|
+
enumerable: true,
|
|
147
|
+
configurable: true,
|
|
148
|
+
writable: true,
|
|
149
|
+
value: void 0
|
|
150
|
+
});
|
|
151
|
+
/** The DH child handlers this owner registered on the multiplexor, once a group operation began. */
|
|
152
|
+
Object.defineProperty(this, "groupRoutes", {
|
|
153
|
+
enumerable: true,
|
|
154
|
+
configurable: true,
|
|
155
|
+
writable: true,
|
|
156
|
+
value: null
|
|
157
|
+
});
|
|
158
|
+
/** When the next scan for resumable Grants is due (host clock, ms); 0 until the first pass. */
|
|
159
|
+
Object.defineProperty(this, "nextSweep", {
|
|
160
|
+
enumerable: true,
|
|
161
|
+
configurable: true,
|
|
162
|
+
writable: true,
|
|
163
|
+
value: 0
|
|
164
|
+
});
|
|
165
|
+
/** Grants a scan found resumable and adoption refused; they are not tried again in this process. */
|
|
166
|
+
Object.defineProperty(this, "refused", {
|
|
167
|
+
enumerable: true,
|
|
168
|
+
configurable: true,
|
|
169
|
+
writable: true,
|
|
170
|
+
value: new Set()
|
|
171
|
+
});
|
|
172
|
+
/** Grants an earlier process left resumable: each is adopted once, or dropped. */
|
|
173
|
+
Object.defineProperty(this, "stale", {
|
|
174
|
+
enumerable: true,
|
|
175
|
+
configurable: true,
|
|
176
|
+
writable: true,
|
|
177
|
+
value: []
|
|
178
|
+
});
|
|
179
|
+
this.discloser = new IpexDiscloser(deps.hby, deps.rgy.store, deps.exc, deps.receiptor);
|
|
180
|
+
}
|
|
181
|
+
/** Whether a pass has work: a live operation, or the one-time scan for resumable Grants has not run. */
|
|
182
|
+
active() {
|
|
183
|
+
return this.live.size > 0 || this.stale.length > 0 || this.deps.clock.now() >= this.nextSweep;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Advance every live operation by one pass. Each pass is bounded by the operation's Grant deadline.
|
|
187
|
+
* The first pass also adopts the pending Grants the stores prove resumable ({@link resumableGrants});
|
|
188
|
+
* that signs and sends nothing.
|
|
189
|
+
*/
|
|
190
|
+
*processOnce() {
|
|
191
|
+
if (this.deps.clock.now() >= this.nextSweep) {
|
|
192
|
+
this.nextSweep = this.deps.clock.now() + ISSUANCE_SWEEP_MS;
|
|
193
|
+
const known = new Set(this.stale.map(({ grant }) => grant));
|
|
194
|
+
this.stale.push(...resumableGrants(this.deps).filter(({ grant }) => !this.live.has(grant) && !known.has(grant) && !this.refused.has(grant) && !this.ended.has(grant)));
|
|
195
|
+
}
|
|
196
|
+
for (const found of [...this.stale]) {
|
|
197
|
+
if (yield* this.settle(found))
|
|
198
|
+
this.stale.splice(this.stale.indexOf(found), 1);
|
|
199
|
+
}
|
|
200
|
+
for (const op of [...this.live.values()]) {
|
|
201
|
+
if (op.busy || !this.live.has(op.grant))
|
|
202
|
+
continue;
|
|
203
|
+
op.busy = true;
|
|
204
|
+
try {
|
|
205
|
+
const left = op.expires === null ? null : op.expires - BigInt(this.deps.clock.now()) * 1000n;
|
|
206
|
+
if (left !== null && left <= 0n)
|
|
207
|
+
this.end(op, "expired", EXPIRED);
|
|
208
|
+
else if ((yield* race([this.advance(op), ...(left === null ? [] : [this.timer(Number(left) / 1000)])])) === "expired")
|
|
209
|
+
this.end(op, "expired", EXPIRED);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
this.end(op, "failed", message(error));
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
op.busy = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** End a live operation: release its DH approval, remember why until the process ends, tell the sink. */
|
|
220
|
+
end(op, state, reason) {
|
|
221
|
+
this.release(op);
|
|
222
|
+
this.ended.set(op.grant, { role: op.role, state, reason });
|
|
223
|
+
this.deps.changes?.push({ kind: "issuance", id: op.grant });
|
|
224
|
+
}
|
|
225
|
+
/** Drop the live entry and the DH approval; a missing approval means the owner already closed. */
|
|
226
|
+
release(op) {
|
|
227
|
+
this.live.delete(op.grant);
|
|
228
|
+
try {
|
|
229
|
+
op.dh.abandon(op.xid);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (!(error instanceof ValidationError))
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Adopt one Grant an earlier process left resumable, without signing: the holder waits for or verifies the
|
|
238
|
+
* update, an issuer whose update is already accepted finishes. A Grant the exchange can no longer resume is
|
|
239
|
+
* dropped and reads from the stores. False means the Keeper is locked and the Grant is retried.
|
|
240
|
+
*/
|
|
241
|
+
*settle(found) {
|
|
242
|
+
if (this.live.has(found.grant))
|
|
243
|
+
return true;
|
|
244
|
+
try {
|
|
245
|
+
found.hab.mgr.requireRegistryRootAccess();
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (error instanceof ValidationError)
|
|
249
|
+
return false;
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
// Another operations table over this runtime reached the Grant first (a command's own, beside the host's).
|
|
253
|
+
if (this.deps.dhkes.dhke(found.hab).holds(found.grant)) {
|
|
254
|
+
this.refused.add(found.grant);
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
yield* this.adopt(found);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
this.refused.add(found.grant);
|
|
262
|
+
// A refusal means the stores no longer prove it resumable and status reads from them; anything else is
|
|
263
|
+
// this Grant's failure alone and must not stop the other operations.
|
|
264
|
+
if (!(error instanceof ValidationError)) {
|
|
265
|
+
this.ended.set(found.grant, { role: found.role, state: "failed", reason: message(error) });
|
|
266
|
+
this.deps.changes?.push({ kind: "issuance", id: found.grant });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
/** Rebuild the live entry of a resumable Grant from the stored Grant and the held roots. */
|
|
272
|
+
*adopt({ hab, grant, role }) {
|
|
273
|
+
const group = isLocalGroupHab(this.deps.hby, hab);
|
|
274
|
+
const { retained, credential, deadline, expires } = selectGrant(this, hab, grant, role, group);
|
|
275
|
+
if (group) {
|
|
276
|
+
this.adoptGroup(hab, grant, role, retained);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const wires = role === "issuer" ? yield* grantWires(retained) : null;
|
|
280
|
+
const dh = this.deps.dhkes.dhke(hab), xid = dh.resume(grant);
|
|
281
|
+
this.ended.delete(grant);
|
|
282
|
+
this.live.set(grant, {
|
|
283
|
+
grant,
|
|
284
|
+
xid,
|
|
285
|
+
role,
|
|
286
|
+
deadline,
|
|
287
|
+
hab,
|
|
288
|
+
dh,
|
|
289
|
+
peer: wires ? retained.serder.sad.ri : retained.serder.pre,
|
|
290
|
+
registry: credential.regid,
|
|
291
|
+
expires,
|
|
292
|
+
issuer: wires ? { credential, ...wires, query: null, sent: true, nextQuery: 0 } : undefined,
|
|
293
|
+
phase: "rootRetained",
|
|
294
|
+
outbox: new DeliveryOutbox(this.deps.retry),
|
|
295
|
+
finished: false,
|
|
296
|
+
busy: false,
|
|
297
|
+
});
|
|
298
|
+
this.deps.changes?.push({ kind: "issuance", id: grant });
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Adopt a group custodian's completed exchange after a restart, signing nothing: the coordinator and custodian
|
|
302
|
+
* the exchange was frozen with are derived from the stores ({@link deriveGroupCustody}), the holder's root is
|
|
303
|
+
* already held, and the group steps finish the Grant against the accepted update. The only signature is the
|
|
304
|
+
* endorsement of the public proof wrapper the original consent already covers.
|
|
305
|
+
*/
|
|
306
|
+
adoptGroup(hab, grant, role, retained) {
|
|
307
|
+
const election = deriveGroupCustody(this.deps, hab, grant);
|
|
308
|
+
const party = GroupParty.resolve(this.deps, hab, election.coordinator.said);
|
|
309
|
+
const group = new GroupIssuance(party, role, retained.serder, retained.nests ?? [], { mode: "resumed" });
|
|
310
|
+
const dh = this.deps.dhkes.dhke(hab), xid = dh.resume(grant, party);
|
|
311
|
+
this.ended.delete(grant);
|
|
312
|
+
this.live.set(grant, this.liveGroup(group, dh, xid, "rootRetained"));
|
|
313
|
+
this.deps.changes?.push({ kind: "issuance", id: grant });
|
|
314
|
+
}
|
|
315
|
+
/** The live entry of a group operation; its lifetime is the Grant's once KRAM has accepted it, else unknown. */
|
|
316
|
+
liveGroup(group, dh, xid, phase) {
|
|
317
|
+
const deadline = this.deps.exc.exchangeDeadline(group.grant), expires = deadline ? timestampMicros(deadline) : null;
|
|
318
|
+
return {
|
|
319
|
+
grant: group.said,
|
|
320
|
+
xid,
|
|
321
|
+
role: group.role,
|
|
322
|
+
deadline,
|
|
323
|
+
hab: group.party.group,
|
|
324
|
+
dh,
|
|
325
|
+
peer: group.peer,
|
|
326
|
+
registry: group.registry,
|
|
327
|
+
expires,
|
|
328
|
+
group,
|
|
329
|
+
phase,
|
|
330
|
+
outbox: new DeliveryOutbox(),
|
|
331
|
+
finished: false,
|
|
332
|
+
busy: false,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
*timer(milliseconds) {
|
|
336
|
+
yield* waitOnClock(this.deps.clock, milliseconds);
|
|
337
|
+
return "expired";
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Queue signed bytes for delivery from the identifier that signed them. Steps decide what to send; {@link drain}
|
|
341
|
+
* sends it, so a member that is briefly offline delays the operation instead of ending it.
|
|
342
|
+
*/
|
|
343
|
+
enqueue(op, message) {
|
|
344
|
+
op.outbox.enqueue(message);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Deliver the queued messages in order. A failure keeps the message and schedules a retry with a doubling delay;
|
|
348
|
+
* it ends the operation only where nothing else bounds it: the Grant deadline ends one that has it, and failures
|
|
349
|
+
* lasting {@link ISSUANCE_RETRY_BOUND_MS} end one that has none yet. True when the outbox is empty.
|
|
350
|
+
*/
|
|
351
|
+
*drain(op) {
|
|
352
|
+
return yield* op.outbox.drain(this.deps.delivery, this.deps.clock, op.expires === null ? this.deps.retry?.boundMs ?? ISSUANCE_RETRY_BOUND_MS : undefined);
|
|
353
|
+
}
|
|
354
|
+
/** One pass: send what is queued, step only once the queue is empty, send what the step queued. */
|
|
355
|
+
*advance(op) {
|
|
356
|
+
if (!(yield* this.drain(op)))
|
|
357
|
+
return "stepped";
|
|
358
|
+
if (!op.finished)
|
|
359
|
+
yield* this.step(op);
|
|
360
|
+
if ((yield* this.drain(op)) && op.finished) {
|
|
361
|
+
this.release(op);
|
|
362
|
+
this.deps.changes?.push({ kind: "issuance", id: op.grant });
|
|
363
|
+
}
|
|
364
|
+
return "stepped";
|
|
365
|
+
}
|
|
366
|
+
/** Record a phase change of a live operation and tell the sink; the group steps call it too. */
|
|
367
|
+
setPhase(op, phase) {
|
|
368
|
+
if (op.phase === phase)
|
|
369
|
+
return;
|
|
370
|
+
op.phase = phase;
|
|
371
|
+
this.deps.changes?.push({ kind: "issuance", id: op.grant });
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Record one group operation the user consented to ({@link approveGroupIssuance},
|
|
375
|
+
* {@link approveGroupIssuanceExchange}, {@link followGroupIssuanceProof}). A holder approves DH here, with its
|
|
376
|
+
* consent; an issuer approves once the group has authenticated the Grant.
|
|
377
|
+
*/
|
|
378
|
+
beginGroup(group) {
|
|
379
|
+
const { deps } = this, { party } = group;
|
|
380
|
+
registerGroupRoutes(this);
|
|
381
|
+
const dh = deps.dhkes.dhke(party.group);
|
|
382
|
+
// A holder approves DH with its consent; an issuer approves once the group has authenticated the Grant.
|
|
383
|
+
const holder = group.role === "holder" && group.mode !== "proofOnly";
|
|
384
|
+
const xid = holder ? dh.approve(group.said, party) : group.xid;
|
|
385
|
+
group.approved = holder;
|
|
386
|
+
const phase = group.mode === "proofOnly"
|
|
387
|
+
? "awaitingAnchor"
|
|
388
|
+
: group.role === "issuer"
|
|
389
|
+
? "awaitingGrantSignatures"
|
|
390
|
+
: "exchangingKeys";
|
|
391
|
+
this.ended.delete(group.said);
|
|
392
|
+
this.live.set(group.said, this.liveGroup(group, dh, xid, phase));
|
|
393
|
+
deps.changes?.push({ kind: "issuance", id: group.said });
|
|
394
|
+
return { grant: group.said, xid };
|
|
395
|
+
}
|
|
396
|
+
/** Whether the live operation for `grant` still holds a message the transport has not taken. */
|
|
397
|
+
owes(grant) {
|
|
398
|
+
return (this.live.get(grant)?.outbox.length ?? 0) > 0;
|
|
399
|
+
}
|
|
400
|
+
/** One pass of the issuer loop, in the order of the CLI loop it replaces. */
|
|
401
|
+
*step(op) {
|
|
402
|
+
if (op.group) {
|
|
403
|
+
op.finished = yield* stepGroup(this, op);
|
|
404
|
+
return "stepped";
|
|
405
|
+
}
|
|
406
|
+
const issuer = op.issuer;
|
|
407
|
+
if (issuer && !issuer.sent) {
|
|
408
|
+
this.enqueue(op, {
|
|
409
|
+
src: op.hab.pre,
|
|
410
|
+
dest: op.peer,
|
|
411
|
+
msg: concatBytes(this.discloser.proofSupport(issuer.raw, op.hab), issuer.grantWire),
|
|
412
|
+
});
|
|
413
|
+
issuer.sent = true;
|
|
414
|
+
}
|
|
415
|
+
while (op.dh.outgoing.length) {
|
|
416
|
+
const queued = op.dh.outgoing.pull();
|
|
417
|
+
if (queued.kin === "sign")
|
|
418
|
+
throw new ValidationError("Individual issuance cannot process group contributions");
|
|
419
|
+
this.enqueue(op, { src: op.hab.pre, dest: queued.dest, msg: queued.msg });
|
|
420
|
+
}
|
|
421
|
+
if (!op.dh.complete(op.xid)) {
|
|
422
|
+
if (issuer?.query && this.deps.clock.now() >= issuer.nextQuery) {
|
|
423
|
+
this.enqueue(op, { src: op.hab.pre, dest: op.peer, msg: () => this.renewed(op, issuer) });
|
|
424
|
+
issuer.nextQuery = this.deps.clock.now() + (this.deps.queryResendMs ?? ISSUANCE_QUERY_RESEND_MS);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else if (issuer)
|
|
428
|
+
yield* this.finish(op, issuer);
|
|
429
|
+
else
|
|
430
|
+
yield* this.receive(op);
|
|
431
|
+
return "stepped";
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* The query to resend now, signed afresh (`Dhke.requery`: same keys and scope, new `dt`, SAID and signatures).
|
|
435
|
+
* An unchanged resend is a KRAM replay once the peer has accepted the original, so it can never reach a peer
|
|
436
|
+
* that lost it; a new query is timely whatever window the peer runs. Re-signing happens only here, inside the
|
|
437
|
+
* live authorisation that began the operation; with the Keeper locked the last query goes again unchanged. A
|
|
438
|
+
* group threshold query cannot be re-dated by one member; group issuance is not run here.
|
|
439
|
+
*/
|
|
440
|
+
renewed(op, issuer) {
|
|
441
|
+
try {
|
|
442
|
+
op.hab.mgr.requireRegistryRootAccess();
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
if (error instanceof ValidationError)
|
|
446
|
+
return issuer.query;
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
issuer.query = op.dh.requery(op.xid);
|
|
450
|
+
return issuer.query;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Holder, DH complete: wait for the issuer's registry update, then verify it and finish the Grant. The
|
|
454
|
+
* issuance is then complete but for the holder's separate Admit consent.
|
|
455
|
+
*/
|
|
456
|
+
*receive(op) {
|
|
457
|
+
if (this.deps.rgy.store.headEvent(op.registry)?.ilk === "rip")
|
|
458
|
+
return this.setPhase(op, "rootRetained");
|
|
459
|
+
this.setPhase(op, "proving");
|
|
460
|
+
yield* op.dh.finish(op.xid);
|
|
461
|
+
if (!this.deps.exc.complete(op.grant))
|
|
462
|
+
throw new ValidationError("Issued Grant remains unaccepted");
|
|
463
|
+
op.finished = true;
|
|
464
|
+
}
|
|
465
|
+
/** DH is complete: stage and anchor the BUP under the live authorisation, finish the Grant, send proof support. */
|
|
466
|
+
*finish(op, issuer) {
|
|
467
|
+
const { rgy, receiptor, derived } = this.deps;
|
|
468
|
+
// An accepted update means a restart came after the anchor: never stage a second one. `Dhke.finish` below
|
|
469
|
+
// proves the head opens this credential as issued against the held root, or the issuance fails.
|
|
470
|
+
if (rgy.store.headEvent(op.registry)?.ilk === "rip") {
|
|
471
|
+
this.setPhase(op, "awaitingAnchor");
|
|
472
|
+
const reg = rgy.regs.get(op.registry);
|
|
473
|
+
const custody = new RegistryCustody({ hby: this.deps.hby, hab: op.hab, registry: rgy.store, store: derived });
|
|
474
|
+
const author = new RegistryAuthor(rgy, receiptor, custody);
|
|
475
|
+
const { event } = author.stage(reg, issuer.credential, "issued", { auths: issuer.auths });
|
|
476
|
+
yield* author.anchor(reg, event, issuer.auths);
|
|
477
|
+
}
|
|
478
|
+
this.setPhase(op, "proving");
|
|
479
|
+
yield* op.dh.finish(op.xid);
|
|
480
|
+
if (!this.deps.exc.complete(op.grant))
|
|
481
|
+
throw new ValidationError("Issued Grant remains unaccepted");
|
|
482
|
+
this.enqueue(op, { src: op.hab.pre, dest: op.peer, msg: this.discloser.proofSupport(issuer.raw, op.hab) });
|
|
483
|
+
op.finished = true;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/** Whether the clock has reached a Grant deadline in microseconds since the epoch. */
|
|
487
|
+
function reached(clock, expires) {
|
|
488
|
+
return expires !== null && BigInt(clock.now()) * 1000n >= expires;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Whether a restarted process can complete this Grant without a new key exchange: it is pending inside its
|
|
492
|
+
* deadline, `hab` is the party `role` names, and {@link resumableExchange} finds the roots. An issuer
|
|
493
|
+
* additionally needs its update already accepted; one that is not anchored cannot be resumed, since the
|
|
494
|
+
* anchor would be signed after the restart. A pure read of stores.
|
|
495
|
+
*/
|
|
496
|
+
function resumable(deps, hab, grant, role) {
|
|
497
|
+
// A group exchange resumes only on the member the stores show to be its custodian.
|
|
498
|
+
if (isLocalGroupHab(deps.hby, hab) && !deriveGroupCustody(deps, hab, grant))
|
|
499
|
+
return false;
|
|
500
|
+
const scope = resumableExchange({ hab, exc: deps.exc }, grant);
|
|
501
|
+
if (!scope || (scope.issuer === hab.pre) !== (role === "issuer"))
|
|
502
|
+
return false;
|
|
503
|
+
return role === "holder" || scope.registries.every((r) => deps.rgy.store.headEvent(r)?.ilk === "bup");
|
|
504
|
+
}
|
|
505
|
+
/** The v2 Grant behind `grant` and the local parties to it, or null when it is not a Grant this runtime is party to. */
|
|
506
|
+
function grantParties(hby, grant) {
|
|
507
|
+
const db = hby.db, serder = db.exns.get([grant]) ?? db.epse.get([grant]);
|
|
508
|
+
if (!serder || serder.said !== grant || serder.route !== "/ipex/grant" || serder.pvrsn.major < 2)
|
|
509
|
+
return null;
|
|
510
|
+
const sender = serder.pre, recipient = serder.sad?.ri;
|
|
511
|
+
const issuer = typeof sender === "string" ? hby.habs.get(sender) : undefined;
|
|
512
|
+
const holder = typeof recipient === "string" ? hby.habs.get(recipient) : undefined;
|
|
513
|
+
return issuer || holder ? { serder, issuer, holder } : null;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Every pending Grant the stores prove a restarted process can finish: one scan of the pending exchange
|
|
517
|
+
* escrow, made once when the owner first runs. A pure read.
|
|
518
|
+
*/
|
|
519
|
+
function resumableGrants(deps) {
|
|
520
|
+
const found = [];
|
|
521
|
+
for (const [[grant]] of [...deps.hby.db.epse.getTopItemIter()]) {
|
|
522
|
+
const parties = grantParties(deps.hby, grant);
|
|
523
|
+
if (!parties)
|
|
524
|
+
continue;
|
|
525
|
+
if (parties.issuer && resumable(deps, parties.issuer, grant, "issuer")) {
|
|
526
|
+
found.push({ hab: parties.issuer, grant, role: "issuer" });
|
|
527
|
+
}
|
|
528
|
+
else if (parties.holder && resumable(deps, parties.holder, grant, "holder")) {
|
|
529
|
+
found.push({ hab: parties.holder, grant, role: "holder" });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return found;
|
|
533
|
+
}
|
|
534
|
+
/** The terminal reading of a pending Grant no process can complete: ended by a restart, or by another exchange. */
|
|
535
|
+
function orphaned(deps, hab, grant) {
|
|
536
|
+
const credential = deps.exc.retainedInput(grant)?.nests?.[0]?.body;
|
|
537
|
+
const registry = credential instanceof SerderACDC ? credential.regid : undefined;
|
|
538
|
+
if (registry && hab.mgr.hasRegistryRoot(hab.pre, registry) && !registryUncommitted(deps.rgy.store, registry)) {
|
|
539
|
+
return ["interrupted", COMMITTED];
|
|
540
|
+
}
|
|
541
|
+
return ["interrupted", RESTARTED];
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Sign a fresh Grant for `credential` and take it into local pending issuance
|
|
545
|
+
* without approving DH or generating a root. A host may deliver or file the
|
|
546
|
+
* returned wire itself and begin later with the Grant SAID.
|
|
547
|
+
*/
|
|
548
|
+
export function* prepareIssuanceGrant(ops, draft) {
|
|
549
|
+
const { deps, discloser } = ops, { credential, options } = draft;
|
|
550
|
+
const hab = credential.regid ? deps.rgy.regs.get(credential.regid)?.hab : undefined;
|
|
551
|
+
if (!hab)
|
|
552
|
+
throw new ValidationError("Fresh issuance requires the issuer's managed local registry");
|
|
553
|
+
requireIndividual(deps, hab);
|
|
554
|
+
hab.mgr.requireRegistryRootAccess();
|
|
555
|
+
const prepared = yield* discloser.prepareIssuanceGrantBody(hab.pre, credential, options);
|
|
556
|
+
const wire = concatBytes(discloser.proofSupport(prepared.raw, hab), exchangeWire(hab, prepared.exn, prepared.nests));
|
|
557
|
+
yield* deps.reactor.processBytes(wire, { local: true });
|
|
558
|
+
yield* deps.protocol.processEscrowsOnce();
|
|
559
|
+
const xid = grantTransactionId(prepared.exn);
|
|
560
|
+
if (!deps.exc.pendingGrant(prepared.exn.said, { xid, sender: hab.pre, recipient: options.recipient })) {
|
|
561
|
+
throw new ValidationError("Fresh Grant did not reach authenticated pending issuance");
|
|
562
|
+
}
|
|
563
|
+
return { grant: prepared.exn.said, wire };
|
|
564
|
+
}
|
|
565
|
+
function requireIndividual(deps, hab) {
|
|
566
|
+
if (isLocalGroupHab(deps.hby, hab)) {
|
|
567
|
+
throw new ValidationError("This finite issuance workflow requires an individual habitat; group issuance needs explicit member coordination");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* The exact unanchored pending Grant a local party may consent to, with its deadline. Refuses a group
|
|
572
|
+
* identifier, an issuance already begun, a Grant that is not this party's, and an issuer without its registry.
|
|
573
|
+
*/
|
|
574
|
+
function selectGrant(ops, hab, grant, role, group = false) {
|
|
575
|
+
const { deps } = ops;
|
|
576
|
+
if (!group)
|
|
577
|
+
requireIndividual(deps, hab);
|
|
578
|
+
if (ops.live.has(grant) || deps.exc.complete(grant)) {
|
|
579
|
+
throw new ValidationError("Issuance was already begun for this Grant");
|
|
580
|
+
}
|
|
581
|
+
const retained = deps.exc.retainedInput(grant, { pending: true });
|
|
582
|
+
const credential = retained?.nests?.[0]?.body;
|
|
583
|
+
if (!retained || !(credential instanceof SerderACDC) || !credential.regid
|
|
584
|
+
|| (role === "issuer" ? retained.serder.pre : retained.serder.sad.ri) !== hab.pre
|
|
585
|
+
|| retained.sourceSeals?.length || retained.ptds?.length || retained.essrs?.length) {
|
|
586
|
+
throw new ValidationError("Select the exact unanchored pending issuance Grant for this local party");
|
|
587
|
+
}
|
|
588
|
+
const reg = deps.rgy.regs.get(credential.regid);
|
|
589
|
+
if (role === "issuer" && (!reg || reg.hab !== hab)) {
|
|
590
|
+
throw new ValidationError("Issuance requires the selected issuer's managed local registry");
|
|
591
|
+
}
|
|
592
|
+
const deadline = deps.exc.exchangeDeadline(retained.serder), expires = timestampMicros(deadline);
|
|
593
|
+
if (!deadline || expires === null)
|
|
594
|
+
throw new Error("Authenticated pending Grant has a missing or corrupt deadline");
|
|
595
|
+
return { retained, credential, deadline, expires };
|
|
596
|
+
}
|
|
597
|
+
/** The Grant body with its nested disclosure (the input of proof support) and the Grant as sent to the holder. */
|
|
598
|
+
function* grantWires(retained) {
|
|
599
|
+
const raw = concatBytes(Counter.makeGVC(Vrsn_2_0), ...yield* call(() => Promise.all((retained.nests ?? []).map((nest) => collectPayload(framePayload(nest))))));
|
|
600
|
+
return { raw, grantWire: yield* storedExchangeWire(retained) };
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* The issuer's one "issue this credential" consent for a prepared Grant ({@link prepareIssuanceGrant}); the
|
|
604
|
+
* issuer is the Grant's sender. Approves the DH exchange, signs the query and records the
|
|
605
|
+
* authorisation, including the later KEL signature that anchors the registry
|
|
606
|
+
* update, in memory. Returns after local commit; `processOnce` does the rest.
|
|
607
|
+
*
|
|
608
|
+
* @throws ValidationError for a Grant that is not this identifier's exact
|
|
609
|
+
* unanchored pending issuance, an unmanaged registry, a group identifier, or
|
|
610
|
+
* an issuance already begun or completed.
|
|
611
|
+
*/
|
|
612
|
+
export function* beginIssuance(ops, grant, auths) {
|
|
613
|
+
const { deps } = ops, hab = grantParties(deps.hby, grant)?.issuer;
|
|
614
|
+
if (!hab)
|
|
615
|
+
throw new ValidationError("Select the exact unanchored pending issuance Grant for this local party");
|
|
616
|
+
const { retained, credential, deadline, expires } = selectGrant(ops, hab, grant, "issuer");
|
|
617
|
+
const { raw, grantWire } = yield* grantWires(retained);
|
|
618
|
+
const dh = deps.dhkes.dhke(hab), xid = dh.approve(grant), query = dh.start(grant);
|
|
619
|
+
ops.ended.delete(grant);
|
|
620
|
+
ops.live.set(grant, {
|
|
621
|
+
grant,
|
|
622
|
+
xid,
|
|
623
|
+
role: "issuer",
|
|
624
|
+
deadline,
|
|
625
|
+
hab,
|
|
626
|
+
dh,
|
|
627
|
+
peer: retained.serder.sad.ri,
|
|
628
|
+
registry: credential.regid,
|
|
629
|
+
expires,
|
|
630
|
+
issuer: { credential, raw, grantWire, query, auths, sent: false, nextQuery: 0 },
|
|
631
|
+
phase: "exchangingKeys",
|
|
632
|
+
outbox: new DeliveryOutbox(ops.deps.retry),
|
|
633
|
+
finished: false,
|
|
634
|
+
busy: false,
|
|
635
|
+
});
|
|
636
|
+
deps.changes?.push({ kind: "issuance", id: grant });
|
|
637
|
+
return { grant, xid };
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* The issuer's one consent to issue a fresh credential: signs the Grant ({@link prepareIssuanceGrant}) and begins
|
|
641
|
+
* the issuance ({@link beginIssuance}) in one step.
|
|
642
|
+
*/
|
|
643
|
+
export function* proposeIssuance(ops, draft, auths) {
|
|
644
|
+
return yield* beginIssuance(ops, (yield* prepareIssuanceGrant(ops, draft)).grant, auths);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Pure read of one issuance: what ended in this process, the live table, and otherwise what the protocol
|
|
648
|
+
* stores prove (see the module doc for the table and what a reload loses). A pending Grant no live
|
|
649
|
+
* operation holds reads `interrupted` (or `expired` once the Grant deadline has passed); that is a
|
|
650
|
+
* statement, not a write.
|
|
651
|
+
*
|
|
652
|
+
* @throws ValidationError when `grant` is not a v2 Grant this runtime's identifiers are party to.
|
|
653
|
+
*/
|
|
654
|
+
export function issuanceStatus(ops, grant) {
|
|
655
|
+
const { deps } = ops, ended = ops.ended.get(grant), live = ops.live.get(grant);
|
|
656
|
+
if (ended)
|
|
657
|
+
return { state: ended.state, role: ended.role, reason: ended.reason };
|
|
658
|
+
if (live && !reached(deps.clock, live.expires)) {
|
|
659
|
+
return {
|
|
660
|
+
state: "active",
|
|
661
|
+
role: live.role,
|
|
662
|
+
phase: live.phase,
|
|
663
|
+
deadline: live.deadline ?? undefined,
|
|
664
|
+
...(live.outbox.failure && { failure: live.outbox.failure }),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const parties = grantParties(deps.hby, grant);
|
|
668
|
+
if (!parties)
|
|
669
|
+
throw new ValidationError("No issuance was begun for this Grant");
|
|
670
|
+
const { serder, issuer, holder } = parties, db = deps.hby.db;
|
|
671
|
+
if (deps.exc.complete(grant)) {
|
|
672
|
+
const admitted = !!db.erpy.get([grant]);
|
|
673
|
+
if (!holder || (issuer && !admitted))
|
|
674
|
+
return { state: "done", role: "issuer", phase: "issued" };
|
|
675
|
+
if (admitted)
|
|
676
|
+
return { state: "done", role: "holder", phase: "admitted" };
|
|
677
|
+
return { state: "active", role: "holder", phase: "awaitingAdmitConsent", needs: "admit" };
|
|
678
|
+
}
|
|
679
|
+
const role = issuer ? "issuer" : "holder", hab = (issuer ?? holder);
|
|
680
|
+
const group = isLocalGroupHab(deps.hby, hab);
|
|
681
|
+
// A group issuer's partial Grant has no accepted lifetime until its members' contributions authenticate it.
|
|
682
|
+
const lifetime = deps.exc.exchangeDeadline(serder) ?? undefined;
|
|
683
|
+
const deadline = lifetime, expires = lifetime ? timestampMicros(lifetime) : null;
|
|
684
|
+
if (!group && (!deadline || expires === null))
|
|
685
|
+
throw new ValidationError("No issuance was begun for this Grant");
|
|
686
|
+
if (reached(deps.clock, expires)) {
|
|
687
|
+
const credential = deps.exc.retainedInput(grant)?.nests?.[0]?.body;
|
|
688
|
+
const registry = credential instanceof SerderACDC ? credential.regid : undefined;
|
|
689
|
+
const late = role === "holder" && registry && deps.rgy.store.headEvent(registry)?.ilk === "bup";
|
|
690
|
+
return { state: "expired", role, reason: late ? EXPIRED_LATE : EXPIRED };
|
|
691
|
+
}
|
|
692
|
+
if (resumable(deps, hab, grant, role))
|
|
693
|
+
return { state: "active", role, phase: "rootRetained", deadline };
|
|
694
|
+
if ((role === "holder" || group) && !heldRoot(deps, hab, grant)) {
|
|
695
|
+
// A group issuer's Grant may still be a partial one that only its members' contributions authenticate.
|
|
696
|
+
if (!group && !deps.exc.retainedInput(grant, { pending: true })) {
|
|
697
|
+
throw new ValidationError("No issuance was begun for this Grant");
|
|
698
|
+
}
|
|
699
|
+
return { state: "active", role, phase: "awaitingExchangeConsent", needs: "exchange", deadline };
|
|
700
|
+
}
|
|
701
|
+
const [state, reason] = orphaned(deps, hab, grant);
|
|
702
|
+
return { state, role, reason };
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Pure read of the Grants waiting for the holder's exchange consent: authenticated pending v2 Grants addressed
|
|
706
|
+
* to a local identifier (to `hab` when given) for which {@link approveIssuanceExchange} would be accepted.
|
|
707
|
+
* `issuanceStatus` throws for a Grant until it is stored; this lists the stored ones, so an embedder needs no poll.
|
|
708
|
+
*/
|
|
709
|
+
export function pendingIssuanceGrants(ops, { hab } = {}) {
|
|
710
|
+
const { deps } = ops, found = [];
|
|
711
|
+
for (const [[grant]] of [...deps.hby.db.epse.getTopItemIter()]) {
|
|
712
|
+
const holder = grantParties(deps.hby, grant)?.holder;
|
|
713
|
+
if (!holder || (hab && holder.pre !== hab.pre))
|
|
714
|
+
continue;
|
|
715
|
+
try {
|
|
716
|
+
const status = issuanceStatus(ops, grant);
|
|
717
|
+
if (status.state !== "active" || status.needs !== "exchange")
|
|
718
|
+
continue;
|
|
719
|
+
const { retained, credential, deadline } = selectGrant(ops, holder, grant, "holder");
|
|
720
|
+
found.push({
|
|
721
|
+
grant,
|
|
722
|
+
holder: holder.pre,
|
|
723
|
+
issuer: retained.serder.pre,
|
|
724
|
+
credential: credential.said,
|
|
725
|
+
registry: credential.regid,
|
|
726
|
+
deadline,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
// Not consentable (a group identifier, a Grant with sources): it is not pending for this holder.
|
|
731
|
+
if (!(error instanceof ValidationError))
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return found;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Pure read of the pending Grants addressed to a local group (to `group` when given) that its members have not
|
|
739
|
+
* consented to: authenticated pending v2 Grants whose issuance status reads `awaitingExchangeConsent`.
|
|
740
|
+
* {@link pendingIssuanceGrants} lists the individual identifiers' Grants and skips groups; this is its group form.
|
|
741
|
+
*/
|
|
742
|
+
export function pendingGroupGrants(ops, { group } = {}) {
|
|
743
|
+
const { deps } = ops, found = [];
|
|
744
|
+
for (const [[grant]] of [...deps.hby.db.epse.getTopItemIter()]) {
|
|
745
|
+
const parties = grantParties(deps.hby, grant), holder = parties?.holder;
|
|
746
|
+
if (!parties || !holder || !isLocalGroupHab(deps.hby, holder) || (group && holder.pre !== group.pre))
|
|
747
|
+
continue;
|
|
748
|
+
try {
|
|
749
|
+
const status = issuanceStatus(ops, grant);
|
|
750
|
+
if (status.state !== "active" || status.needs !== "exchange")
|
|
751
|
+
continue;
|
|
752
|
+
const credential = deps.exc.retainedInput(grant, { pending: true })?.nests?.[0]?.body;
|
|
753
|
+
const deadline = deps.exc.exchangeDeadline(parties.serder);
|
|
754
|
+
if (!(credential instanceof SerderACDC) || !credential.regid || !deadline)
|
|
755
|
+
continue;
|
|
756
|
+
found.push({
|
|
757
|
+
grant,
|
|
758
|
+
holder: holder.pre,
|
|
759
|
+
issuer: parties.serder.pre,
|
|
760
|
+
credential: credential.said,
|
|
761
|
+
registry: credential.regid,
|
|
762
|
+
deadline,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
if (!(error instanceof ValidationError))
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return found;
|
|
771
|
+
}
|
|
772
|
+
/** Whether `hab` holds a registry root for the credential registry this Grant discloses. */
|
|
773
|
+
function heldRoot(deps, hab, grant) {
|
|
774
|
+
const credential = deps.exc.retainedInput(grant)?.nests?.[0]?.body;
|
|
775
|
+
const registry = credential instanceof SerderACDC ? credential.regid : undefined;
|
|
776
|
+
return !!registry && hab.mgr.hasRegistryRoot(hab.pre, registry);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Observe until the issuance is done or ended, or the budget expires. Starts no
|
|
780
|
+
* worker and drives nothing: someone else must call `processOnce`.
|
|
781
|
+
*/
|
|
782
|
+
export function* waitForIssuance(ops, grant, { timeoutMs, clock = ops.deps.clock, until = (status) => status.state !== "active" }) {
|
|
783
|
+
// A phase is not reached while the step that produced it still owes a delivery: the host would stop its workers first.
|
|
784
|
+
const settled = yield* observeUntil(() => until(issuanceStatus(ops, grant)) && !ops.owes(grant), {
|
|
785
|
+
timeoutMs,
|
|
786
|
+
clock,
|
|
787
|
+
});
|
|
788
|
+
const status = issuanceStatus(ops, grant);
|
|
789
|
+
return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* The holder's consent to the DH exchange for one pending Grant addressed to a local identifier. Approves DH and records the
|
|
793
|
+
* consent; `processOnce` then answers the issuer's query, waits for its registry update and finishes the Grant.
|
|
794
|
+
* Admitting the credential is a separate consent ({@link admitIssuance}).
|
|
795
|
+
*/
|
|
796
|
+
export function approveIssuanceExchange(ops, grant) {
|
|
797
|
+
const { deps } = ops, hab = grantParties(deps.hby, grant)?.holder;
|
|
798
|
+
if (!hab)
|
|
799
|
+
throw new ValidationError("Select the exact unanchored pending issuance Grant for this local party");
|
|
800
|
+
const { retained, credential, deadline, expires } = selectGrant(ops, hab, grant, "holder");
|
|
801
|
+
const dh = deps.dhkes.dhke(hab), xid = dh.approve(grant);
|
|
802
|
+
ops.ended.delete(grant);
|
|
803
|
+
ops.live.set(grant, {
|
|
804
|
+
grant,
|
|
805
|
+
xid,
|
|
806
|
+
role: "holder",
|
|
807
|
+
deadline,
|
|
808
|
+
hab,
|
|
809
|
+
dh,
|
|
810
|
+
peer: retained.serder.pre,
|
|
811
|
+
registry: credential.regid,
|
|
812
|
+
expires,
|
|
813
|
+
phase: "exchangingKeys",
|
|
814
|
+
outbox: new DeliveryOutbox(ops.deps.retry),
|
|
815
|
+
finished: false,
|
|
816
|
+
busy: false,
|
|
817
|
+
});
|
|
818
|
+
deps.changes?.push({ kind: "issuance", id: grant });
|
|
819
|
+
return { grant, xid };
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* The holder's consent to admit one accepted Grant: signs the Admit, accepts it locally and records it. Works for
|
|
823
|
+
* a Grant finished by this module and for any other accepted v2 Grant. Returns
|
|
824
|
+
* before delivery so an embedder can record its own inventory first; see {@link deliverIssuanceAdmit}.
|
|
825
|
+
*
|
|
826
|
+
* @throws ValidationError for a Grant that is not accepted and addressed to a local identifier, an admitted Grant, or a group identifier.
|
|
827
|
+
*/
|
|
828
|
+
export function* admitIssuance(deps, grant, options = {}) {
|
|
829
|
+
const { message = "", gvrsn, auths } = options, hab = grantParties(deps.hby, grant)?.holder;
|
|
830
|
+
if (!hab)
|
|
831
|
+
throw new ValidationError("Admit requires an accepted v2 Grant addressed to a local identifier");
|
|
832
|
+
if (deps.hby.db.erpy.get([grant]))
|
|
833
|
+
throw new ValidationError("Issuance cannot be admitted in its state");
|
|
834
|
+
const prepared = yield* new IpexAdmitter(deps.hby, deps.rgy.store, deps.exc).prepareAdmit(grant, hab.pre, {
|
|
835
|
+
consent: true,
|
|
836
|
+
auths,
|
|
837
|
+
message,
|
|
838
|
+
gvrsn,
|
|
839
|
+
receiptor: deps.receiptor,
|
|
840
|
+
});
|
|
841
|
+
const wire = concatBytes(prepared.support, prepared.wire);
|
|
842
|
+
yield* deps.reactor.processBytes(wire, { local: true });
|
|
843
|
+
yield* deps.protocol.processEscrowsOnce();
|
|
844
|
+
if (!deps.exc.complete(prepared.admit.said))
|
|
845
|
+
throw new ValidationError("Local ACDC v2 Admit was not accepted.");
|
|
846
|
+
deps.changes?.push({ kind: "issuance", id: grant });
|
|
847
|
+
return { admit: prepared.admit, src: hab.pre, wire, dest: prepared.admit.sad.ri };
|
|
848
|
+
}
|
|
849
|
+
/** Deliver an accepted Admit; needs the host's delivery worker. Returns the destinations reached. */
|
|
850
|
+
export function* deliverIssuanceAdmit(delivery, admitted) {
|
|
851
|
+
return yield* delivery.deliver({ src: admitted.src, dest: admitted.dest, msg: admitted.wire });
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Rebuild an Admit this process signed earlier so it can be delivered again, after a failed delivery or a
|
|
855
|
+
* restart. The wire is rebuilt from the stored signatures or source seal, so it is the bytes first made and
|
|
856
|
+
* nothing is signed. Delivery stays with {@link deliverIssuanceAdmit}. A peer that already took the Admit
|
|
857
|
+
* may drop the repeat as a replay.
|
|
858
|
+
*
|
|
859
|
+
* @throws ValidationError for an Admit no local identifier signed or whose retained factors no longer authenticate.
|
|
860
|
+
*/
|
|
861
|
+
export function* rebuildIssuanceAdmit(deps, admit) {
|
|
862
|
+
const retained = deps.exc.retainedInput(admit), serder = retained?.serder;
|
|
863
|
+
const hab = serder?.pre ? deps.hby.habs.get(serder.pre) : undefined;
|
|
864
|
+
if (!retained || !serder || serder.route !== "/ipex/admit" || !hab || typeof serder.sad?.ri !== "string") {
|
|
865
|
+
throw new ValidationError("No Admit was signed by a local identifier with this SAID");
|
|
866
|
+
}
|
|
867
|
+
const support = retained.sourceSeals?.length
|
|
868
|
+
? new IpexDiscloser(deps.hby, deps.rgy.store, deps.exc, deps.receiptor).kel(hab)
|
|
869
|
+
: new Uint8Array();
|
|
870
|
+
return {
|
|
871
|
+
admit: serder,
|
|
872
|
+
src: hab.pre,
|
|
873
|
+
wire: concatBytes(support, yield* storedExchangeWire(retained)),
|
|
874
|
+
dest: serder.sad.ri,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Local abandon: release the DH approval (ephemeral keys, permanent refusal of
|
|
879
|
+
* this operation) and read `abandoned` until the process ends. Does not message the peer and does
|
|
880
|
+
* not undo a retained root or a registry update that was already anchored.
|
|
881
|
+
* Does nothing to an issuance that is already done or ended.
|
|
882
|
+
*
|
|
883
|
+
* @throws ValidationError when no issuance was begun for `grant`.
|
|
884
|
+
*/
|
|
885
|
+
export function abandonIssuance(ops, grant, reason = "Abandoned by the user") {
|
|
886
|
+
const status = issuanceStatus(ops, grant);
|
|
887
|
+
if (status.state !== "active")
|
|
888
|
+
return;
|
|
889
|
+
const live = ops.live.get(grant);
|
|
890
|
+
if (live)
|
|
891
|
+
ops.end(live, "abandoned", reason);
|
|
892
|
+
else {
|
|
893
|
+
ops.ended.set(grant, { role: status.role, state: "abandoned", reason });
|
|
894
|
+
ops.deps.changes?.push({ kind: "issuance", id: grant });
|
|
895
|
+
}
|
|
896
|
+
}
|