@coordination-network/user-client 0.0.1 → 0.1.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/README.md +18 -10
- package/dist/index.cjs +20 -10
- package/dist/index.js +20 -10
- package/package.json +7 -6
- package/src/http-submitter.ts +26 -8
- package/src/request.ts +2 -3
- package/types/index.d.ts +11 -5
package/README.md
CHANGED
|
@@ -20,10 +20,10 @@ console.log(networkId) // 0x-prefixed bytes32
|
|
|
20
20
|
console.log(displayMetaStealthAddress) // "st:eth:0x..."
|
|
21
21
|
|
|
22
22
|
// InMemoryIdentityRegistry and InMemoryMemberServer are test/demo fakes standing in for the
|
|
23
|
-
// registry's read model and the network member's server. A real deployment
|
|
24
|
-
//
|
|
25
|
-
// member server
|
|
26
|
-
// seams" below.
|
|
23
|
+
// registry's read model and the network member's server. A real deployment splits this across
|
|
24
|
+
// two processes: the holder's app signs the request and hands it to the network member's app,
|
|
25
|
+
// which submits it to its own member server, carrying that server's own token — see
|
|
26
|
+
// "Dependency-injection seams" below.
|
|
27
27
|
const registry = new InMemoryIdentityRegistry()
|
|
28
28
|
const memberServer = new InMemoryMemberServer(registry, { chainId: 1, verifyingContract: "0x..." })
|
|
29
29
|
|
|
@@ -58,7 +58,7 @@ if (result.status === "registered") {
|
|
|
58
58
|
| `InMemoryIdentityRegistry` | class | In-memory `RegistryReader` fake for tests/local dev. `registerIdentity` is write-once. | `registerIdentity` throws `SubmissionFailedError` (`InvalidMetaStealthAddressLength` or `AlreadyRegistered`). |
|
|
59
59
|
| `InMemoryMemberServer` | class | In-memory `RegistrationSubmitter` fake standing in for an approved network member's server. | `submit` throws `SubmissionFailedError` on `InvalidRegistrationRequest` or `AlreadyRegistered`. |
|
|
60
60
|
| `createHttpRegistrationSubmitter` | function | Builds a `RegistrationSubmitter` that POSTs to a member server's `/v1/registrations` endpoint over `fetch`. | `HttpSubmissionError` for every non-submitted response and on a network failure. |
|
|
61
|
-
| `HttpSubmissionError` | class | Extends `SubmissionFailedError`. Carries `status`, `reason` (`"alreadyRegistered" \| "rejected" \| "unavailable" \| "network" \| "invalidResponse"`), and
|
|
61
|
+
| `HttpSubmissionError` | class | Extends `SubmissionFailedError`. Carries `status`, `reason` (`"alreadyRegistered" \| "rejected" \| "unavailable" \| "unauthorized" \| "network" \| "invalidResponse"`), and when sent, `serverCode`/`metaStealthAddress`/`matchesRequest`. | — |
|
|
62
62
|
| `UserClientError` | class | Base error class for this package, discriminated by `code: UserClientErrorCode`. | — |
|
|
63
63
|
| `InvalidMetaStealthAddressError` | class | Re-exported from `@coordination-network/identity`. Extends its `KeygenError`, not `UserClientError`, and names the malformed key. Thrown when encoding a meta-stealth address. | — |
|
|
64
64
|
| `SubmissionFailedError` | class | Extends `UserClientError` (`code: "SUBMISSION_FAILED"`). Thrown when a `RegistrationSubmitter.submit` call fails, including on a lost duplicate-registration race (see below). | — |
|
|
@@ -79,26 +79,34 @@ git diff --exit-code packages/user-client/tests/vectors/registration-vectors.jso
|
|
|
79
79
|
|
|
80
80
|
**Implementing `RegistryReader` against a chain.** `IdentityRegistry` stores the mapping as `bytes`, so its generated getter answers an unregistered `networkId` with empty bytes. viem and ethers surface that as `"0x"`, and the port requires `null`. `register()` rejects anything that is neither `null` nor a 66-byte entry rather than reading it as an existing registration, but an adapter should map the empty answer to `null` itself.
|
|
81
81
|
|
|
82
|
-
`RegistryReader` (read an identity's current registration) and `RegistrationSubmitter` (submit a new one) are dependency-injection interfaces, not concrete transports. On chain, writes are gated on an approved network member's Semaphore proof (`IdentityRegistry.registerIdentity(networkId, metaStealthAddress, proof)`; the proof comes from `@coordination-network/network-member-proof`). The user's signature is consent the member server verifies off chain with `verifyRegistrationRequest` before it builds the proof and submits; the contract never reads it. No credential check sits on this path (#46). The gas-sponsorship model is settled ([`specs/05-stealth-addresses.md`](../../specs/05-stealth-addresses.md), "Gas sponsorship"): registration stays self-submitted and self-paid by the network-member server, and only post-registration transactions go through the shared amortised Semaphore paymaster. Any future transport — a relayer, a meta-transaction relay, a bundler — plugs in as a new `RegistrationSubmitter` implementation without changing this package's API. `InMemoryIdentityRegistry` and `InMemoryMemberServer` are in-memory fakes for tests and local development; `@coordination-network/network-member-client`'s `IdentityRegistryClient` satisfies `RegistryReader` (it maps the getter's empty bytes to `null`); `createHttpRegistrationSubmitter` (below) is the
|
|
82
|
+
`RegistryReader` (read an identity's current registration) and `RegistrationSubmitter` (submit a new one) are dependency-injection interfaces, not concrete transports. On chain, writes are gated on an approved network member's Semaphore proof (`IdentityRegistry.registerIdentity(networkId, metaStealthAddress, proof)`; the proof comes from `@coordination-network/network-member-proof`). The user's signature is consent the member server verifies off chain with `verifyRegistrationRequest` before it builds the proof and submits; the contract never reads it. No credential check sits on this path (#46). The gas-sponsorship model is settled ([`specs/05-stealth-addresses.md`](../../specs/05-stealth-addresses.md), "Gas sponsorship"): registration stays self-submitted and self-paid by the network-member server, and only post-registration transactions go through the shared amortised Semaphore paymaster. Any future transport — a relayer, a meta-transaction relay, a bundler — plugs in as a new `RegistrationSubmitter` implementation without changing this package's API. `InMemoryIdentityRegistry` and `InMemoryMemberServer` are in-memory fakes for tests and local development; `@coordination-network/network-member-client`'s `IdentityRegistryClient` satisfies `RegistryReader` (it maps the getter's empty bytes to `null`); `createHttpRegistrationSubmitter` (below) is the member-side HTTP `RegistrationSubmitter`, for a process the organisation runs that holds its `memberServerToken`; it posts to `apps/issuer-service`'s `POST /v1/registrations`, which verifies consent with `verifyRegistrationRequest` and submits it via `network-member-client`'s `submitRegistration`.
|
|
83
83
|
|
|
84
84
|
### HTTP submitter
|
|
85
85
|
|
|
86
|
-
`createHttpRegistrationSubmitter({ url })` POSTs
|
|
86
|
+
A member-side process calls `submitter.submit(request)` with the `RegistrationRequest` the holder already built and signed with this package's own `buildRegistrationRequest`. `createHttpRegistrationSubmitter({ url, memberServerToken })` POSTs that request as JSON to `${url}/v1/registrations` and maps the member server's response onto this package's types:
|
|
87
87
|
|
|
88
88
|
```typescript
|
|
89
89
|
import { createHttpRegistrationSubmitter } from "@coordination-network/user-client"
|
|
90
90
|
|
|
91
|
-
const submitter = createHttpRegistrationSubmitter({
|
|
92
|
-
|
|
91
|
+
const submitter = createHttpRegistrationSubmitter({
|
|
92
|
+
url: "https://member.example",
|
|
93
|
+
memberServerToken: process.env.MEMBER_SERVER_TOKEN
|
|
94
|
+
})
|
|
95
|
+
await submitter.submit(request)
|
|
93
96
|
```
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
`memberServerToken` is the Network Member Server's own bearer token (`apps/issuer-service`'s `MEMBER_SERVER_TOKEN`), sent as `Authorization: Bearer <memberServerToken>` when set; the registration route now requires it. That token belongs to a member-side process the organisation itself runs, not to a holder — never pass one from a holder's wallet or app bundle.
|
|
99
|
+
|
|
100
|
+
Don't pair the Quickstart's `register()` composition with a `createHttpRegistrationSubmitter` that holds `memberServerToken` anywhere but a trusted single process such as a local testbed; on a holder's device, give `register()` a `RegistrationSubmitter` that hands the signed request to the member's app instead.
|
|
101
|
+
|
|
102
|
+
A `201` resolves `submit()` with `{ txHash }`. A `200` (the member server's `alreadyRegistered` response) throws `HttpSubmissionError` (`reason: "alreadyRegistered"`) carrying the registry's current `metaStealthAddress` and `matchesRequest`; a member-side caller has no device secret to re-run `register()` with, so treat those fields as the answer — the identity is already registered, and `matchesRequest` says whether it's already this request's address. A caller that is itself running `register()` (a trusted single process) can instead just re-run it: its own `registry.getIdentity` read will now see the entry and return the `alreadyRegistered` result instead. `400` throws `reason: "rejected"`; `401` (a missing or wrong `memberServerToken`) throws `reason: "unauthorized"`; `429` and `503` both throw `reason: "unavailable"` carrying the server's own `serverCode` (`"RATE_LIMITED"` for a `429`); and a network failure throws `reason: "network"`. `HttpSubmissionError` never includes the request body or its signature.
|
|
96
103
|
|
|
97
104
|
## Security notes
|
|
98
105
|
|
|
99
106
|
- **`status: "registered"` means submitted, not confirmed.** `register()` returns as soon as the injected `RegistrationSubmitter` resolves. It does not wait for inclusion or check a receipt, and `receipt.txHash` is `null` for a submitter that queues rather than broadcasts. A transaction that is mined and reverts also resolves here.
|
|
100
107
|
- **An existing entry is reported, not verified, and can change.** Only `registerIdentity` refuses a duplicate; an approved network member can `updateIdentity` or `removeIdentity` any entry at any time. If `alreadyRegistered` comes back with `matchesDerived: false`, this device's keys do not derive the meta-stealth address its `networkId` currently resolves to, and that is recoverable only through a member calling `updateIdentity`. A `matchesDerived: true` result is not permanent either — re-read `getIdentity` and compare against `metaStealthAddressFromKeys` whenever the mapping is load-bearing.
|
|
101
108
|
- **Consent is checked by the member server, not the chain.** `IdentityRegistry` gates writes on a Semaphore proof, not on the user's signature. A member server that skips `verifyRegistrationRequest` can register any `metaStealthAddress` under any `networkId`, since its membership proof covers whatever pair it submits. Comparing `metaStealthAddress` with `derivedMetaStealthAddress` (the bullet above) is how a user detects that.
|
|
109
|
+
- **`memberServerToken` leaking is worse than one bad registration.** Anyone who extracts it can act as that member on every member-side route, not just registration.
|
|
102
110
|
- **Duplicate-registration races.** If two `register()` calls for the same `networkId` are in flight at once, both will see no existing registration and both will attempt `submit()`. The losing submission surfaces as a `SubmissionFailedError` (`AlreadyRegistered`, in `InMemoryMemberServer`'s case). The caller should simply retry `register()`: the retry's `registry.getIdentity` lookup will now find the winner's registration and return the `alreadyRegistered` result instead of erroring.
|
|
103
111
|
- **React Native / Hermes.** `@coordination-network/identity`'s barrel loads on Hermes without a global `Buffer` — `onboard` and `register` don't need one. An app that also depends on something else expecting Node's `Buffer` may still want to set a polyfill globally for that code, but this package's own path does not require it.
|
|
104
112
|
|
package/dist/index.cjs
CHANGED
|
@@ -196,9 +196,8 @@ function verifyRegistrationRequest(request, expected) {
|
|
|
196
196
|
if (signature.hasHighS())
|
|
197
197
|
return false;
|
|
198
198
|
const recoveredPublicKey = signature.recoverPublicKey(hexToBytes(digest)).toBytes(false);
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// itself checks no signer at all.
|
|
199
|
+
// Compare all 32 bytes of the hash. A partial match would leave bytes unauthenticated and let
|
|
200
|
+
// one key claim many ids. The contract itself checks no signer at all.
|
|
202
201
|
return identity.computeNetworkId(recoveredPublicKey).toLowerCase() === request.networkId.toLowerCase();
|
|
203
202
|
}
|
|
204
203
|
catch {
|
|
@@ -311,9 +310,11 @@ async function register(params) {
|
|
|
311
310
|
|
|
312
311
|
/**
|
|
313
312
|
* Thrown by {@link createHttpRegistrationSubmitter}. `alreadyRegistered`
|
|
314
|
-
* carries the registry's current
|
|
315
|
-
*
|
|
316
|
-
* `
|
|
313
|
+
* carries the registry's current `metaStealthAddress` and `matchesRequest`
|
|
314
|
+
* as the answer — a member-side caller has no device secret to re-run
|
|
315
|
+
* `register()` with. A caller that is itself running `register()` (a
|
|
316
|
+
* trusted single process) can instead just re-run it, since its own
|
|
317
|
+
* `registry.getIdentity` read will now return `alreadyRegistered`.
|
|
317
318
|
*/
|
|
318
319
|
class HttpSubmissionError extends SubmissionFailedError {
|
|
319
320
|
constructor(message, init) {
|
|
@@ -342,14 +343,17 @@ function parseTxHash(value) {
|
|
|
342
343
|
* Never logs or throws the request body or its signature — only the response
|
|
343
344
|
* status and, where sent, the server's own error code.
|
|
344
345
|
*/
|
|
345
|
-
function createHttpRegistrationSubmitter({ url, fetch: fetchImpl = fetch }) {
|
|
346
|
+
function createHttpRegistrationSubmitter({ url, fetch: fetchImpl = fetch, memberServerToken }) {
|
|
346
347
|
return {
|
|
347
348
|
async submit(request) {
|
|
348
349
|
let response;
|
|
349
350
|
try {
|
|
350
351
|
response = await fetchImpl(`${url}/v1/registrations`, {
|
|
351
352
|
method: "POST",
|
|
352
|
-
headers: {
|
|
353
|
+
headers: {
|
|
354
|
+
"Content-Type": "application/json",
|
|
355
|
+
...(memberServerToken === undefined ? {} : { Authorization: `Bearer ${memberServerToken}` })
|
|
356
|
+
},
|
|
353
357
|
body: JSON.stringify(request)
|
|
354
358
|
});
|
|
355
359
|
}
|
|
@@ -395,9 +399,15 @@ function createHttpRegistrationSubmitter({ url, fetch: fetchImpl = fetch }) {
|
|
|
395
399
|
reason: "rejected"
|
|
396
400
|
});
|
|
397
401
|
}
|
|
398
|
-
if (response.status ===
|
|
402
|
+
if (response.status === 401) {
|
|
403
|
+
throw new HttpSubmissionError("Registration submission was not authorized", {
|
|
404
|
+
status: 401,
|
|
405
|
+
reason: "unauthorized"
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if (response.status === 429 || response.status === 503) {
|
|
399
409
|
throw new HttpSubmissionError("Registration service is unavailable", {
|
|
400
|
-
status:
|
|
410
|
+
status: response.status,
|
|
401
411
|
reason: "unavailable",
|
|
402
412
|
serverCode: body?.code
|
|
403
413
|
});
|
package/dist/index.js
CHANGED
|
@@ -195,9 +195,8 @@ function verifyRegistrationRequest(request, expected) {
|
|
|
195
195
|
if (signature.hasHighS())
|
|
196
196
|
return false;
|
|
197
197
|
const recoveredPublicKey = signature.recoverPublicKey(hexToBytes(digest)).toBytes(false);
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
// itself checks no signer at all.
|
|
198
|
+
// Compare all 32 bytes of the hash. A partial match would leave bytes unauthenticated and let
|
|
199
|
+
// one key claim many ids. The contract itself checks no signer at all.
|
|
201
200
|
return computeNetworkId(recoveredPublicKey).toLowerCase() === request.networkId.toLowerCase();
|
|
202
201
|
}
|
|
203
202
|
catch {
|
|
@@ -310,9 +309,11 @@ async function register(params) {
|
|
|
310
309
|
|
|
311
310
|
/**
|
|
312
311
|
* Thrown by {@link createHttpRegistrationSubmitter}. `alreadyRegistered`
|
|
313
|
-
* carries the registry's current
|
|
314
|
-
*
|
|
315
|
-
* `
|
|
312
|
+
* carries the registry's current `metaStealthAddress` and `matchesRequest`
|
|
313
|
+
* as the answer — a member-side caller has no device secret to re-run
|
|
314
|
+
* `register()` with. A caller that is itself running `register()` (a
|
|
315
|
+
* trusted single process) can instead just re-run it, since its own
|
|
316
|
+
* `registry.getIdentity` read will now return `alreadyRegistered`.
|
|
316
317
|
*/
|
|
317
318
|
class HttpSubmissionError extends SubmissionFailedError {
|
|
318
319
|
constructor(message, init) {
|
|
@@ -341,14 +342,17 @@ function parseTxHash(value) {
|
|
|
341
342
|
* Never logs or throws the request body or its signature — only the response
|
|
342
343
|
* status and, where sent, the server's own error code.
|
|
343
344
|
*/
|
|
344
|
-
function createHttpRegistrationSubmitter({ url, fetch: fetchImpl = fetch }) {
|
|
345
|
+
function createHttpRegistrationSubmitter({ url, fetch: fetchImpl = fetch, memberServerToken }) {
|
|
345
346
|
return {
|
|
346
347
|
async submit(request) {
|
|
347
348
|
let response;
|
|
348
349
|
try {
|
|
349
350
|
response = await fetchImpl(`${url}/v1/registrations`, {
|
|
350
351
|
method: "POST",
|
|
351
|
-
headers: {
|
|
352
|
+
headers: {
|
|
353
|
+
"Content-Type": "application/json",
|
|
354
|
+
...(memberServerToken === undefined ? {} : { Authorization: `Bearer ${memberServerToken}` })
|
|
355
|
+
},
|
|
352
356
|
body: JSON.stringify(request)
|
|
353
357
|
});
|
|
354
358
|
}
|
|
@@ -394,9 +398,15 @@ function createHttpRegistrationSubmitter({ url, fetch: fetchImpl = fetch }) {
|
|
|
394
398
|
reason: "rejected"
|
|
395
399
|
});
|
|
396
400
|
}
|
|
397
|
-
if (response.status ===
|
|
401
|
+
if (response.status === 401) {
|
|
402
|
+
throw new HttpSubmissionError("Registration submission was not authorized", {
|
|
403
|
+
status: 401,
|
|
404
|
+
reason: "unauthorized"
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (response.status === 429 || response.status === 503) {
|
|
398
408
|
throw new HttpSubmissionError("Registration service is unavailable", {
|
|
399
|
-
status:
|
|
409
|
+
status: response.status,
|
|
400
410
|
reason: "unavailable",
|
|
401
411
|
serverCode: body?.code
|
|
402
412
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coordination-network/user-client",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Single-call onboarding and registration for the coordination network: derive device keys, encode the meta-stealth address, and register networkId -> metaStealthAddress on IdentityRegistry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,16 +36,17 @@
|
|
|
36
36
|
"build": "rimraf dist types && rollup -c rollup.config.mjs"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@coordination-network/identity": "^0.0
|
|
39
|
+
"@coordination-network/identity": "^0.1.0",
|
|
40
40
|
"@noble/curves": "^1.9.7",
|
|
41
41
|
"@noble/hashes": "^1.8.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@coordination-network/network-member-client": "^0.0
|
|
45
|
-
"@coordination-network/network-member-proof": "^0.0
|
|
44
|
+
"@coordination-network/network-member-client": "^0.1.0",
|
|
45
|
+
"@coordination-network/network-member-proof": "^0.1.0",
|
|
46
46
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
47
|
-
"@semaphore-protocol/group": "4.14.
|
|
48
|
-
"@semaphore-protocol/identity": "4.14.
|
|
47
|
+
"@semaphore-protocol/group": "4.14.4",
|
|
48
|
+
"@semaphore-protocol/identity": "4.14.4",
|
|
49
|
+
"ffjavascript": "0.3.1",
|
|
49
50
|
"rimraf": "^6.1.3",
|
|
50
51
|
"rollup": "^4.62.2",
|
|
51
52
|
"rollup-plugin-cleanup": "^3.2.1",
|
package/src/http-submitter.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { hexToBytes } from "./bytes"
|
|
|
3
3
|
import { TX_HASH_LENGTH } from "./constants"
|
|
4
4
|
import { SubmissionFailedError } from "./errors"
|
|
5
5
|
|
|
6
|
-
export type HttpSubmissionReason =
|
|
6
|
+
export type HttpSubmissionReason =
|
|
7
|
+
"alreadyRegistered" | "rejected" | "unavailable" | "unauthorized" | "network" | "invalidResponse"
|
|
7
8
|
|
|
8
9
|
export interface HttpSubmissionErrorInit {
|
|
9
10
|
status?: number
|
|
@@ -18,9 +19,11 @@ export interface HttpSubmissionErrorInit {
|
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Thrown by {@link createHttpRegistrationSubmitter}. `alreadyRegistered`
|
|
21
|
-
* carries the registry's current
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
22
|
+
* carries the registry's current `metaStealthAddress` and `matchesRequest`
|
|
23
|
+
* as the answer — a member-side caller has no device secret to re-run
|
|
24
|
+
* `register()` with. A caller that is itself running `register()` (a
|
|
25
|
+
* trusted single process) can instead just re-run it, since its own
|
|
26
|
+
* `registry.getIdentity` read will now return `alreadyRegistered`.
|
|
24
27
|
*/
|
|
25
28
|
export class HttpSubmissionError extends SubmissionFailedError {
|
|
26
29
|
readonly status?: number
|
|
@@ -43,6 +46,10 @@ export class HttpSubmissionError extends SubmissionFailedError {
|
|
|
43
46
|
export interface HttpRegistrationSubmitterOptions {
|
|
44
47
|
url: string
|
|
45
48
|
fetch?: typeof fetch
|
|
49
|
+
/** Bearer token for the Network Member Server's member-side routes, sent as
|
|
50
|
+
* `Authorization: Bearer <memberServerToken>` when set. The server's own token — never a
|
|
51
|
+
* holder's, so it must not ship in a wallet or app bundle. */
|
|
52
|
+
memberServerToken?: string
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
interface SubmittedBody {
|
|
@@ -77,7 +84,8 @@ function parseTxHash(value: unknown): Hex | null {
|
|
|
77
84
|
*/
|
|
78
85
|
export function createHttpRegistrationSubmitter({
|
|
79
86
|
url,
|
|
80
|
-
fetch: fetchImpl = fetch
|
|
87
|
+
fetch: fetchImpl = fetch,
|
|
88
|
+
memberServerToken
|
|
81
89
|
}: HttpRegistrationSubmitterOptions): RegistrationSubmitter {
|
|
82
90
|
return {
|
|
83
91
|
async submit(request: RegistrationRequest): Promise<SubmissionReceipt> {
|
|
@@ -85,7 +93,10 @@ export function createHttpRegistrationSubmitter({
|
|
|
85
93
|
try {
|
|
86
94
|
response = await fetchImpl(`${url}/v1/registrations`, {
|
|
87
95
|
method: "POST",
|
|
88
|
-
headers: {
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
...(memberServerToken === undefined ? {} : { Authorization: `Bearer ${memberServerToken}` })
|
|
99
|
+
},
|
|
89
100
|
body: JSON.stringify(request)
|
|
90
101
|
})
|
|
91
102
|
} catch (error) {
|
|
@@ -134,9 +145,16 @@ export function createHttpRegistrationSubmitter({
|
|
|
134
145
|
})
|
|
135
146
|
}
|
|
136
147
|
|
|
137
|
-
if (response.status ===
|
|
148
|
+
if (response.status === 401) {
|
|
149
|
+
throw new HttpSubmissionError("Registration submission was not authorized", {
|
|
150
|
+
status: 401,
|
|
151
|
+
reason: "unauthorized"
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (response.status === 429 || response.status === 503) {
|
|
138
156
|
throw new HttpSubmissionError("Registration service is unavailable", {
|
|
139
|
-
status:
|
|
157
|
+
status: response.status,
|
|
140
158
|
reason: "unavailable",
|
|
141
159
|
serverCode: (body as ErrorBody | null)?.code
|
|
142
160
|
})
|
package/src/request.ts
CHANGED
|
@@ -79,9 +79,8 @@ export function verifyRegistrationRequest(request: RegistrationRequest, expected
|
|
|
79
79
|
|
|
80
80
|
const recoveredPublicKey = signature.recoverPublicKey(hexToBytes(digest)).toBytes(false)
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// itself checks no signer at all.
|
|
82
|
+
// Compare all 32 bytes of the hash. A partial match would leave bytes unauthenticated and let
|
|
83
|
+
// one key claim many ids. The contract itself checks no signer at all.
|
|
85
84
|
return computeNetworkId(recoveredPublicKey).toLowerCase() === request.networkId.toLowerCase()
|
|
86
85
|
} catch {
|
|
87
86
|
return false
|
package/types/index.d.ts
CHANGED
|
@@ -145,7 +145,7 @@ declare class InMemoryMemberServer implements RegistrationSubmitter {
|
|
|
145
145
|
declare function onboard(deviceSecret: Uint8Array | Hex): Onboarding;
|
|
146
146
|
declare function register(params: RegisterParams): Promise<RegisterResult>;
|
|
147
147
|
|
|
148
|
-
type HttpSubmissionReason = "alreadyRegistered" | "rejected" | "unavailable" | "network" | "invalidResponse";
|
|
148
|
+
type HttpSubmissionReason = "alreadyRegistered" | "rejected" | "unavailable" | "unauthorized" | "network" | "invalidResponse";
|
|
149
149
|
interface HttpSubmissionErrorInit {
|
|
150
150
|
status?: number;
|
|
151
151
|
reason: HttpSubmissionReason;
|
|
@@ -158,9 +158,11 @@ interface HttpSubmissionErrorInit {
|
|
|
158
158
|
}
|
|
159
159
|
/**
|
|
160
160
|
* Thrown by {@link createHttpRegistrationSubmitter}. `alreadyRegistered`
|
|
161
|
-
* carries the registry's current
|
|
162
|
-
*
|
|
163
|
-
* `
|
|
161
|
+
* carries the registry's current `metaStealthAddress` and `matchesRequest`
|
|
162
|
+
* as the answer — a member-side caller has no device secret to re-run
|
|
163
|
+
* `register()` with. A caller that is itself running `register()` (a
|
|
164
|
+
* trusted single process) can instead just re-run it, since its own
|
|
165
|
+
* `registry.getIdentity` read will now return `alreadyRegistered`.
|
|
164
166
|
*/
|
|
165
167
|
declare class HttpSubmissionError extends SubmissionFailedError {
|
|
166
168
|
readonly status?: number;
|
|
@@ -173,13 +175,17 @@ declare class HttpSubmissionError extends SubmissionFailedError {
|
|
|
173
175
|
interface HttpRegistrationSubmitterOptions {
|
|
174
176
|
url: string;
|
|
175
177
|
fetch?: typeof fetch;
|
|
178
|
+
/** Bearer token for the Network Member Server's member-side routes, sent as
|
|
179
|
+
* `Authorization: Bearer <memberServerToken>` when set. The server's own token — never a
|
|
180
|
+
* holder's, so it must not ship in a wallet or app bundle. */
|
|
181
|
+
memberServerToken?: string;
|
|
176
182
|
}
|
|
177
183
|
/**
|
|
178
184
|
* HTTP `RegistrationSubmitter` for `apps/issuer-service`'s `POST /v1/registrations`.
|
|
179
185
|
* Never logs or throws the request body or its signature — only the response
|
|
180
186
|
* status and, where sent, the server's own error code.
|
|
181
187
|
*/
|
|
182
|
-
declare function createHttpRegistrationSubmitter({ url, fetch: fetchImpl }: HttpRegistrationSubmitterOptions): RegistrationSubmitter;
|
|
188
|
+
declare function createHttpRegistrationSubmitter({ url, fetch: fetchImpl, memberServerToken }: HttpRegistrationSubmitterOptions): RegistrationSubmitter;
|
|
183
189
|
|
|
184
190
|
export { DIGEST_LENGTH, EIP712_DOMAIN_TYPE, HttpSubmissionError, IDENTITY_REGISTRY_DOMAIN_NAME, IDENTITY_REGISTRY_DOMAIN_VERSION, InMemoryIdentityRegistry, InMemoryMemberServer, NETWORK_ID_LENGTH, REGISTER_IDENTITY_TYPE, REQUEST_VERSION, RegistryReadFailedError, SCHEME_ID, SIGNATURE_LENGTH, SubmissionFailedError, TX_HASH_LENGTH, UserClientError, buildRegistrationRequest, createHttpRegistrationSubmitter, identityRegistryDomainSeparator, metaStealthAddressFromKeys, onboard, register, registerIdentityDigest, registerIdentityStructHash, signRegisterIdentity, verifyRegistrationRequest };
|
|
185
191
|
export type { ChainDomain, Hex, HttpRegistrationSubmitterOptions, HttpSubmissionErrorInit, HttpSubmissionReason, Onboarding, RegisterParams, RegisterResult, RegistrationRequest, RegistrationSubmitter, RegistryReader, SubmissionReceipt, UserClientErrorCode };
|