provared 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +209 -0
- package/README.md +1 -1
- package/docs/guide.md +3 -3
- package/package.json +1 -1
- package/page/style.css +7 -0
- package/src/passkey-browser.d.ts +3 -1
- package/src/passkey-browser.js +31 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,216 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 (1 November 2026)
|
|
4
|
+
|
|
5
|
+
Version 0.8.0 changes the library's browser part, `src/passkey-browser.js`, which a web page uses to have a person sign with a passkey. A new function, `signChallengeWithChosenPasskey`, lets a page whose browser has forgotten which passkey signed before ask the device to offer the passkeys it holds for the website; the page must then check what comes back against the key it expects. Every signing function now also says which passkey signed (`credentialId`). What changes for a user: every other signing function now refuses, before the device is asked, a passkey that does not name its identifier as text of at least one character, with a `PasskeyError` of code `cancelled` and the words "No passkey was named, so nothing was signed." A missing or non-text identifier already failed with that code, with other words; a passkey whose identifier was empty text was handed to the device, and now throws instead. The signing page uses all this to open as a small window for another page (the Provared application), sign a new permission, a renewal or a cancellation there, and hand the record back; the signing page is served at sign.prova.red and is not in the npm package. Nothing else in the library changes: no record's bytes, no rule of the checker, nothing in the Python version but its version number, so a book checks with 0.8.0 exactly as with 0.7.0. Beside the library, in this repository, with entries in the list below: `provared-mcp-executor` 0.2.0, with rules on the content of a tool call, already published on npm on 1 November 2026; and a reader of Agent Middleware's receipts, `provared-agent-middleware`, not published. The framework packages `provared-langchain` and `pydantic-ai-provared` stay at 0.2.0, unchanged.
|
|
6
|
+
|
|
7
|
+
- **Versions.** The library is 0.8.0, in `package.json` and `python/pyproject.toml`. The Hermes plugin (not published) asks for `provared>=0.4.0,<0.9`, where it asked for `<0.8`, which left 0.8.0 out; the test workflow checks that bound. `examples/frameworks/package-lock.json` gives the library's version as 0.8.0. No other package's range changes: the others' tests run against the repository's own library whatever range they state, and the ranges of the published `provared-mcp-executor` 0.2.0 (`^0.7.0`) and `provared-claude-code` 0.1.0 (`>=0.6.0 <0.8.0`) are left as published, since neither needs anything of 0.8.0. The README, the guide, the Python README and three pages say 0.8.0, and the guide gives the checking page's new fingerprint.
|
|
8
|
+
|
|
9
|
+
- **The signing page opened as a small window by another page** (the
|
|
10
|
+
Provared application, which signs nothing itself). `page/sign.html` and
|
|
11
|
+
`page/sign-page.js` take a request after `#request=` in the address: a
|
|
12
|
+
new permission, a renewal or a cancellation, with the fields to fill,
|
|
13
|
+
the earlier signed permission, the passkey's public key, the address to
|
|
14
|
+
hand the result back to, and a short text handed back unchanged. The
|
|
15
|
+
page trusts the earlier permission only once the library's checker has
|
|
16
|
+
confirmed its passkey signature, then shows what the new permission
|
|
17
|
+
changes against it, part by part. It shows the return address before
|
|
18
|
+
signing, and warns, asking for a second press, before a new passkey is
|
|
19
|
+
made, before an agent's key set is signed for the first time on that
|
|
20
|
+
device, before a passkey other than the earlier one signs, and where the
|
|
21
|
+
return address is not one of the application's own two addresses,
|
|
22
|
+
exactly (`https://app.prova.red:8443` and `https://app.prova.red`;
|
|
23
|
+
`http://localhost` is taken and always warned of). The warnings are
|
|
24
|
+
worked out again at every press, and a press signs only where every
|
|
25
|
+
warning that stands then was shown and confirmed; a passkey prompt that
|
|
26
|
+
fails, or a passkey this browser forgets, takes the confirmation back. A
|
|
27
|
+
passkey is made only where the warning that one will be made was among
|
|
28
|
+
those confirmed. Where the browser has forgotten which passkey signed
|
|
29
|
+
before, the device is asked to offer the passkeys it holds for the
|
|
30
|
+
website, and what it signs is kept only if the checker passes it with
|
|
31
|
+
the expected public key. A cancellation is signed only by the passkey
|
|
32
|
+
that signed the permission, at this page's own website name and
|
|
33
|
+
address: a request to cancel one signed elsewhere, or with a passkey for
|
|
34
|
+
another website name (a parent name included), is refused, and a
|
|
35
|
+
cancellation never makes a passkey. A cancellation for an address that
|
|
36
|
+
is not the application's asks for a second press too. The signed record
|
|
37
|
+
is sent by `postMessage` to the exact return address, and the page says
|
|
38
|
+
it was handed over only once that page answers; it closes once that page
|
|
39
|
+
answers, from the window that opened it and from the return address
|
|
40
|
+
exactly, that it kept the record (`{ provared: 'received', kind, state,
|
|
41
|
+
kept }`), and stays open with the file offered where it says it did not,
|
|
42
|
+
or says nothing within ten seconds. With no window that opened it, the
|
|
43
|
+
page offers the file first, then a button that goes back with the record
|
|
44
|
+
after `#signed=`. A signature that arrives after the permission was
|
|
45
|
+
changed is not kept. A page shown inside another page still offers
|
|
46
|
+
nothing.
|
|
47
|
+
- `src/passkey-browser.js`: `signChallengeWithChosenPasskey(challenge,
|
|
48
|
+
rpId)` asks the device to offer the passkeys it holds for the website,
|
|
49
|
+
for a caller that checks the answer against the key it expects; the
|
|
50
|
+
answer of every signing function now names the passkey's identifier
|
|
51
|
+
(`credentialId`). Every other function still refuses a passkey with no
|
|
52
|
+
identifier, so that no caller is offered a choice it did not ask for.
|
|
53
|
+
- Tests: `test/sign-request.test.mjs` (the request, the return address,
|
|
54
|
+
what changed, the messages each way) and `test/sign-window.test.mjs`
|
|
55
|
+
(the window driven in a browser with a virtual passkey, at phone and
|
|
56
|
+
desktop width: a new permission, a renewal and a cancellation handed
|
|
57
|
+
back; the wrong passkey refused; the answer that the record was not
|
|
58
|
+
kept, and no answer; the way back by a link for all three kinds; a
|
|
59
|
+
request that cannot be read; a framed page),
|
|
60
|
+
`test/sign-window-review.test.mjs` (the cases an independent review
|
|
61
|
+
found, at made-up https addresses: a failed passkey prompt, a passkey
|
|
62
|
+
forgotten between two presses, a cancellation for a parent website name
|
|
63
|
+
or another address, a cancellation for an unknown address, a record
|
|
64
|
+
sent to a page at another address, an answer from another window, a
|
|
65
|
+
change while the passkey is asked, a cancellation by another passkey)
|
|
66
|
+
and `test/passkey-browser-guard.test.mjs` (a passkey with no identifier
|
|
67
|
+
is refused before the device is asked).
|
|
68
|
+
- **After an independent re-check of the signing window, 1 November
|
|
69
|
+
2026:** once a cancellation asked for by a request has been signed and
|
|
70
|
+
handed back (or offered to take back by a link), the "sign the
|
|
71
|
+
cancellation" button stays off: while the page that asked waits, after
|
|
72
|
+
it says it did not keep it, and after no answer. Before, it was switched
|
|
73
|
+
on again, and a second press signed a second cancellation with no new
|
|
74
|
+
warning. The file stays offered; a new request starts the page afresh.
|
|
75
|
+
Held by `test/sign-window.test.mjs`.
|
|
76
|
+
|
|
77
|
+
- **`provared-mcp-executor` 0.2.0** (published on npm on 1 November 2026
|
|
78
|
+
and listed in the official MCP Registry as
|
|
79
|
+
`io.github.provared/mcp-executor`; the library stays at 0.7.0, and the
|
|
80
|
+
executor still asks for `^0.7.0`). It carries the rules
|
|
81
|
+
on the content of a change, below (choices X56 to X77), and names itself
|
|
82
|
+
for the official MCP Registry: `package.json` gains
|
|
83
|
+
`"mcpName": "io.github.provared/mcp-executor"`, and
|
|
84
|
+
`mcp-executor/server.json` describes the package for the registry, with
|
|
85
|
+
the command line `stdio --config <settings file>` (choices X78 to X81 in
|
|
86
|
+
`mcp-executor/DESIGN.md`). `server.json` is not in the npm package; the
|
|
87
|
+
registry's publishing tool reads it from the folder. Beside it,
|
|
88
|
+
`mcp-executor/demo/` (not in the package): the executor in front of a
|
|
89
|
+
made-up tool server with two harmless tools, set up on its first start
|
|
90
|
+
with a permission signed by the library's development stand-in for a
|
|
91
|
+
passkey, so that a directory of MCP servers can start it with no
|
|
92
|
+
settings and no secret; a `Dockerfile` on the official `node` image,
|
|
93
|
+
pinned by digest; its records prove nothing about anyone, as its README
|
|
94
|
+
says (choices X82 to X89). One new test starts it as a real process.
|
|
95
|
+
After an independent review (choices X90 to X99): the README and the
|
|
96
|
+
design say that only an Owner of the organisation may publish to the
|
|
97
|
+
registry, with a token that can read its members, and only after the
|
|
98
|
+
npm version is out; the demonstration uses only an empty folder or one
|
|
99
|
+
it marked as its own, keeps each set-up in a folder of its own and never
|
|
100
|
+
renames or empties a folder, writes its paths afresh on every start (so
|
|
101
|
+
it survives a move or a new Node.js), lets two starts at once on an
|
|
102
|
+
empty folder each set up, says plainly when a folder cannot be written,
|
|
103
|
+
and its server compares the made-up credential's value; the container
|
|
104
|
+
is built from four named files, with a packed executor handed over as
|
|
105
|
+
a context of its own and never copied in. Four more tests. After a
|
|
106
|
+
second, narrow review (choice X100): an empty mark left by a start that
|
|
107
|
+
was stopped no longer refuses the folder for good, a file error inside
|
|
108
|
+
the folder gets the same plain words, and a name that is a file is said
|
|
109
|
+
to be one. Two more tests.
|
|
110
|
+
- **The MCP executor tests rules on the content of a change before a
|
|
111
|
+
call is let through** (F-157, F-158; `mcp-executor/rules.mjs`; choices
|
|
112
|
+
X56 to X70 in `mcp-executor/DESIGN.md`). A permission allows kinds of
|
|
113
|
+
action; being allowed to change a file is not being allowed to change
|
|
114
|
+
everything the file controls. The new setting `contentRules` names a
|
|
115
|
+
JSON file of rules: each names a tool, says which calls it applies to
|
|
116
|
+
(`when`: conditions on the arguments), where the proposed content is
|
|
117
|
+
and how to read it (`content`: a JSON Pointer, as JSON text or as plain
|
|
118
|
+
data) and what must hold in it (`test`: `equals`, `oneOf`, `absent`,
|
|
119
|
+
`matches` a plain glob, each at a JSON Pointer, with `orAbsent`). A rule
|
|
120
|
+
with no `test` refuses every call it applies to, which is how a tool
|
|
121
|
+
that carries only a partial edit is closed: the executor reads no file
|
|
122
|
+
and keeps no content. A call a rule refuses (`content-rule`), or whose
|
|
123
|
+
content the rule cannot read exactly (`content-unreadable`: not JSON, a
|
|
124
|
+
member named twice or twice once folded, a number beyond 2^53 - 1,
|
|
125
|
+
nothing at the pointer, not text, a text too long to match), is not
|
|
126
|
+
sent; the model is told which rule and what it asked for, never a value
|
|
127
|
+
from the arguments, and a denial is written. Every receipt and denial
|
|
128
|
+
names `executor.rules`, the SHA-256 of the rules file's parsed content
|
|
129
|
+
in the one form of RFC 8785; a denial by a rule also names
|
|
130
|
+
`executor.rule`, the fingerprint of the rule's name; both are labels.
|
|
131
|
+
The person puts the same fingerprint into the permission's
|
|
132
|
+
`agent.software` (`keys` prints the entry); where the permission states
|
|
133
|
+
software and the rules in force are not among it, the executor does not
|
|
134
|
+
start; where it states nothing, it warns. `check` reports the rules and
|
|
135
|
+
whether the permission states them; `compare` reports which rules
|
|
136
|
+
fingerprints the records name and whether a slip states each. The
|
|
137
|
+
README says plainly what the rules can never promise: an edit by
|
|
138
|
+
another route, a tool whose arguments do not show the resulting file, a
|
|
139
|
+
file type the executor cannot read, a pattern that matches characters
|
|
140
|
+
rather than files. Content in YAML, TOML or line-based forms is later
|
|
141
|
+
work. No record and no rule of the format changes. 12 new tests, among
|
|
142
|
+
them a label change let through and the same edit with a hidden setting
|
|
143
|
+
switched on refused.
|
|
144
|
+
After an independent review (choices X71 to X77): with `unlisted`
|
|
145
|
+
`forward`, a content rule for a tool that `tools` does not name stops
|
|
146
|
+
the start, since such a call was sent on before any content rule was
|
|
147
|
+
tested and the warning said the opposite; a permission whose
|
|
148
|
+
`agent.software` states rules on content (an entry named `Content rules
|
|
149
|
+
of …`) stops the start where the settings name no rules file, and
|
|
150
|
+
`compare` lists the executor's receipts and denials under such a slip
|
|
151
|
+
that name no rules file (`withoutRules`); a `/` or a `\` in a pattern
|
|
152
|
+
matches either separator, as promised, so `**/settings.json` matches
|
|
153
|
+
`C:\proj\settings.json`; a rules file holds at most 20,000 pattern parts
|
|
154
|
+
in all, which keeps one call to about a quarter of a second where a
|
|
155
|
+
chosen file took 9.6 seconds; the file is decoded as strict UTF-8; a
|
|
156
|
+
JSON Pointer with a `~` not followed by `0` or `1` is refused. Four more
|
|
157
|
+
limits are stated in the README: a `when` value that is not text does
|
|
158
|
+
not match; a member in other capitals in the content passes an
|
|
159
|
+
`absent` or `orAbsent` test; a path with a trailing full stop is another
|
|
160
|
+
text; JSON text inside a JSON string is not read. 7 new tests.
|
|
161
|
+
- **A reader of another project's receipts: Agent Middleware (AMW)**,
|
|
162
|
+
`integrations/agent-middleware/` (`provared-agent-middleware` 0.1.0, not
|
|
163
|
+
published): the first of the plan's readers of the closest other
|
|
164
|
+
projects' receipts. It checks an AMW permit or receipt by the gateway's
|
|
165
|
+
published Ed25519 key (the key found by `kid`, its status read; an
|
|
166
|
+
envelope compared with the signed content; AMW's own six answers,
|
|
167
|
+
reproduced for every one of its 96 published test cases, which are
|
|
168
|
+
copied into `test/vectors/` under their Apache License 2.0 with a notice
|
|
169
|
+
of where they came from), maps each verified receipt onto the fields
|
|
170
|
+
the checker compares (the tool as the action, `credits_charged` as the
|
|
171
|
+
amount in a unit and scale the reader states, `created_at` as the time,
|
|
172
|
+
`permit_id` as the permit it names), and judges a set of receipts
|
|
173
|
+
against a Provared slip that names the AMW gateway in `with`: the
|
|
174
|
+
checker's own codes where they apply (`action-not-allowed`,
|
|
175
|
+
`prohibited`, `outside-valid-time`, `over-limit`, `over-each-limit`,
|
|
176
|
+
`over-count-limit`, `over-period-limit`, `amount-missing`,
|
|
177
|
+
`approval-missing`), with words that say "AMW receipt"; codes of its
|
|
178
|
+
own (`amw-permit-…`) where only the AMW permit can be compared; and a
|
|
179
|
+
stated list of what cannot be judged (no chain, so omission and cut
|
|
180
|
+
ends do not show; no countersignature; no signature of the person on
|
|
181
|
+
the AMW permit; approvals by identifier only; which agent; with whom).
|
|
182
|
+
A denied AMW receipt is shown as the gateway's refusal and never
|
|
183
|
+
counted against the agent; a receipt that does not verify is not shown.
|
|
184
|
+
`receiptDocument` carries an AMW receipt by its fingerprint in a
|
|
185
|
+
Provared receipt's `details` under the name `agent-middleware.receipt`.
|
|
186
|
+
A command, `provared-agent-middleware verify | judge`, with the
|
|
187
|
+
checker's exit codes. Written from AMW's published data and the
|
|
188
|
+
standards alone; no AMW program was read or run. Its tests run in the
|
|
189
|
+
test workflow beside the other packages.
|
|
190
|
+
- **The AMW reader after an independent review**, each fix held by a
|
|
191
|
+
test (`test/review.test.mjs`): a slip's countersignature condition, or
|
|
192
|
+
an approval named by identifier only, is listed on the receipt it
|
|
193
|
+
applies to (`notJudged`), and such a receipt is never said to be within
|
|
194
|
+
the slip (`withinSlip` is `null`, not known) nor gives exit code 0
|
|
195
|
+
(before, both gave "within" and 0); a P-256 key over content that says
|
|
196
|
+
"Ed25519" reads `unsupported` (before, `verified`); signed text that is
|
|
197
|
+
not well-formed Unicode, names a member twice in one object, or gives a
|
|
198
|
+
`kid` that is not text is `malformed`; times are compared to the
|
|
199
|
+
nanosecond (before, cut to milliseconds, so two receipts 3,599.9996
|
|
200
|
+
seconds apart fell in different hours); a receipt handed over twice is
|
|
201
|
+
counted once and listed as a duplicate; a receipt that fails its check
|
|
202
|
+
shows no identifier (the envelope's is not signed), only its place; the
|
|
203
|
+
command shows control and invisible characters as a mark, and escapes
|
|
204
|
+
them in `--json`; `verify` loads no library, and the README's `judge`
|
|
205
|
+
command works from a copy of the repository; `cannotShow` also states
|
|
206
|
+
that cancellations cannot be handed over, that outcomes other than
|
|
207
|
+
success are left out of totals, and that retired keys check receipts
|
|
208
|
+
of any date. The package no longer pins a development copy of
|
|
209
|
+
`provared` 0.6.0, which its peer range excludes.
|
|
210
|
+
|
|
3
211
|
## 0.7.0 (1 November 2026)
|
|
4
212
|
|
|
213
|
+
|
|
5
214
|
Version 0.7.0 makes three more document names part of the format (section 27 of the format description: `provared.tool-name`, `provared.tool-call-id` and `provared.tool-result`), and lets whoever checks a record name the addresses at which a permission must have been signed (`issuerOrigins`, Python `issuer_origins`, `--origin` on the command line, and a box on the checking page), so that a permission that another page under the passkey's website name had the passkey sign, by presenting it as a sign-in, is refused. Beside the library, two programs are published on npm for the first time, each at 0.1.0: `provared-mcp-executor`, an executor for MCP tool servers, which holds the tool servers' credentials, refuses a tool call the permission does not allow, and countersigns each receipt with keys of its own; and `provared-claude-code`, the recorder beside Claude Code, which can now also be installed as a Claude Code plugin. Releases are now published from the author's signed tag by a GitHub workflow, and each npm version waits for the author's approval with a passkey. What a reader must know: no record's bytes change, and a book written by 0.6.0 checks as it did, since 0.6.0 refused the three new names and so never wrote them; but a book that names any of the three new names checks in full only with a checker of 0.7.0 or later, because a checker of 0.5.0 or 0.6.0 refuses it (`bad-field`, as for every `provared.` name it does not know) and one of 0.4.0 or earlier reads the names as labels. The MCP executor writes two of them in every receipt, so its books need 0.7.0. The framework packages `provared-langchain` and `pydantic-ai-provared` stay at 0.2.0, unchanged: they ask for `provared` 0.6.0 or later, and what they write is the same with 0.7.0.
|
|
6
215
|
|
|
7
216
|
- **Versions.** The library is 0.7.0, in `package.json` and `python/pyproject.toml`. Ranges on the library: `provared-mcp-executor` and the OpenTelemetry bridge (not published) now ask for `^0.7.0`, where they asked for `>0.6.0 <0.8.0`; the recorder beside Claude Code, the gateway and the sealer (neither of the last two published) ask for `>=0.6.0 <0.8.0`, where they asked for `^0.6.0`, which left 0.7.0 out (the recorder's `package-lock.json`, which a Claude Code plugin installs from, still holds 0.6.0, which the new range accepts, and the plugin's test of that lockfile now takes a range of that form as well as a caret range); the Hermes plugin (not published) asks for `>=0.4.0,<0.8`, where it asked for `<0.7`. The README and the guide name the two packages now on npm, with their install lines.
|
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ each part protects against, is in
|
|
|
171
171
|
|
|
172
172
|
## Status
|
|
173
173
|
|
|
174
|
-
**Version 0.
|
|
174
|
+
**Version 0.8.0, a draft.** Published as it is, by one author in their
|
|
175
175
|
own time, built with AI tools since 3 October 2026; fixes as time
|
|
176
176
|
allows. The code has been through more than 80 rounds of review by AI reviewers,
|
|
177
177
|
each reviewer a fresh session given the code, the documents and a brief
|
package/docs/guide.md
CHANGED
|
@@ -6,7 +6,7 @@ Everything about the format, the library, the checker, the stub writer beside an
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
**Version 0.
|
|
9
|
+
**Version 0.8.0, a draft.** Published as it is, by one author in their
|
|
10
10
|
own time; fixes as time allows. The format may still change, so do not
|
|
11
11
|
rely on a draft record staying checkable.
|
|
12
12
|
|
|
@@ -153,9 +153,9 @@ be loaded from anywhere. The tool prints the file's SHA-256 fingerprint,
|
|
|
153
153
|
so that a copy can be compared with it. (Tried on 8 October 2026 in the
|
|
154
154
|
three browser builds this repository is tested with, and on 13 October
|
|
155
155
|
2026 in Chrome 155, Edge 155 and Brave 155.) The file for
|
|
156
|
-
version 0.
|
|
156
|
+
version 0.8.0 is attached to the release on GitHub. Its SHA-256
|
|
157
157
|
fingerprint is
|
|
158
|
-
`
|
|
158
|
+
`f9f2b6d747de66d7330dc2e7d817991e21208e16571f7f5d4e913d2c29ad4174`.
|
|
159
159
|
|
|
160
160
|
**Run the demonstration in a browser.**
|
|
161
161
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "provared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Proof of what an AI agent was allowed to do, and what it then did. A passkey-signed permission with limits a machine can check, a signed receipt for each action, and a free checker. Evidence, not a verdict.",
|
|
5
5
|
"author": "Pavel Izmaylov",
|
|
6
6
|
"license": "Apache-2.0 AND CC-BY-4.0",
|
package/page/style.css
CHANGED
|
@@ -151,3 +151,10 @@ details.more textarea { width: 100%; box-sizing: border-box; font-family: ui-mon
|
|
|
151
151
|
|
|
152
152
|
/* The mark: a permission slip with a signature, beside a heading or the site's name. */
|
|
153
153
|
.mark { vertical-align: -0.2em; margin-right: 0.4em; }
|
|
154
|
+
|
|
155
|
+
/* The signing page: the warnings before a passkey is asked, and what a
|
|
156
|
+
renewal changes against the earlier permission. */
|
|
157
|
+
.warnings { margin: 0.8rem 0; padding: 0.6rem 0.9rem 0.6rem 2rem; border: 1px solid var(--bad); border-radius: 0.5rem; }
|
|
158
|
+
.warnings li { margin: 0.3rem 0; }
|
|
159
|
+
dt.changed { color: var(--seal); font-weight: 600; }
|
|
160
|
+
#cancel-of, #issuer-name { max-width: 100%; box-sizing: border-box; }
|
package/src/passkey-browser.d.ts
CHANGED
|
@@ -17,4 +17,6 @@ export function signSlipWithPasskey(fields: Omit<SlipFields, 'issuer'>, passkey:
|
|
|
17
17
|
export function approveWithPasskey(request: Request & { slip: Fingerprint; when?: number | Date }, passkey: StoredPasskey): Promise<SignedRecord>;
|
|
18
18
|
export function cancelWithPasskey(request: { slip: Fingerprint; when?: number | Date; id?: Base64url }, passkey: StoredPasskey): Promise<SignedRecord>;
|
|
19
19
|
/** Sign the challenge of anything prepared for a passkey; hand the result to the matching assemble function. */
|
|
20
|
-
export function signChallengeWithPasskey(challenge: Uint8Array, passkey: StoredPasskey): Promise<Assertion>;
|
|
20
|
+
export function signChallengeWithPasskey(challenge: Uint8Array, passkey: StoredPasskey): Promise<Assertion & { credentialId: string }>;
|
|
21
|
+
/** Sign a challenge with a passkey the person chooses from those the device holds for the website; check the answer against the key you expect before relying on it. */
|
|
22
|
+
export function signChallengeWithChosenPasskey(challenge: Uint8Array, rpId: string): Promise<Assertion & { credentialId: string }>;
|
package/src/passkey-browser.js
CHANGED
|
@@ -87,15 +87,24 @@ export async function createPasskey({ name, site }) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// Ask the passkey to sign a challenge: the device asks the person first.
|
|
90
|
-
|
|
90
|
+
// Only where the caller asks for it ("choose"), and names no identifier (a
|
|
91
|
+
// passkey this browser has forgotten), the device offers every passkey it
|
|
92
|
+
// holds for the website and the person chooses; the answer then says which
|
|
93
|
+
// was chosen, and the caller must check it against the key it expects
|
|
94
|
+
// before it relies on it. Everywhere else a passkey with no identifier is
|
|
95
|
+
// refused, so that no caller is offered a choice it did not ask for.
|
|
96
|
+
async function signedBy(passkey, challenge, refused, { choose = false } = {}) {
|
|
91
97
|
const container = credentials();
|
|
98
|
+
if (!choose && (!passkey || typeof passkey.credentialId !== 'string' || !passkey.credentialId)) {
|
|
99
|
+
throw new PasskeyError('cancelled', 'No passkey was named, so nothing was signed.');
|
|
100
|
+
}
|
|
92
101
|
let assertion;
|
|
93
102
|
try {
|
|
94
103
|
assertion = await container.get({
|
|
95
104
|
publicKey: {
|
|
96
105
|
challenge,
|
|
97
106
|
rpId: passkey.rpId,
|
|
98
|
-
allowCredentials: [{ type: 'public-key', id: fromBase64url(passkey.credentialId) }],
|
|
107
|
+
...(!choose ? { allowCredentials: [{ type: 'public-key', id: fromBase64url(passkey.credentialId) }] } : {}),
|
|
99
108
|
userVerification: 'required',
|
|
100
109
|
},
|
|
101
110
|
});
|
|
@@ -107,6 +116,7 @@ async function signedBy(passkey, challenge, refused) {
|
|
|
107
116
|
authenticatorData: new Uint8Array(r.authenticatorData),
|
|
108
117
|
clientDataJSON: new Uint8Array(r.clientDataJSON),
|
|
109
118
|
signature: new Uint8Array(r.signature),
|
|
119
|
+
credentialId: toBase64url(new Uint8Array(assertion.rawId)),
|
|
110
120
|
};
|
|
111
121
|
}
|
|
112
122
|
|
|
@@ -169,3 +179,22 @@ export async function signChallengeWithPasskey(challenge, passkey) {
|
|
|
169
179
|
credentials();
|
|
170
180
|
return signedBy(passkey, challenge, 'Nothing was signed. The request was cancelled or the device refused it.');
|
|
171
181
|
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Sign a challenge with a passkey the person chooses, where this browser
|
|
185
|
+
* has forgotten which passkey it should ask (a passkey made on another
|
|
186
|
+
* device, or this browser's storage cleared). The device offers every
|
|
187
|
+
* passkey it holds for the website, and the person chooses one; nothing
|
|
188
|
+
* new is made. What comes back names the passkey chosen by its identifier,
|
|
189
|
+
* but not its public key: the caller must put the answer into the record
|
|
190
|
+
* and check it with the library's checker against the key it expects, and
|
|
191
|
+
* only then remember the identifier beside that key.
|
|
192
|
+
*
|
|
193
|
+
* @param {Uint8Array} challenge the "challenge" of what was prepared
|
|
194
|
+
* @param {string} rpId the website the passkey belongs to (this page's hostname)
|
|
195
|
+
* @returns {Promise<{authenticatorData: Uint8Array, clientDataJSON: Uint8Array, signature: Uint8Array, credentialId: string}>}
|
|
196
|
+
*/
|
|
197
|
+
export async function signChallengeWithChosenPasskey(challenge, rpId) {
|
|
198
|
+
credentials();
|
|
199
|
+
return signedBy({ rpId }, challenge, 'Nothing was signed. The request was cancelled, or this device holds no passkey for this website.', { choose: true });
|
|
200
|
+
}
|