@mdbase-dev/connect 0.1.0-beta.22 → 0.1.0-beta.24

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 CHANGED
@@ -33,7 +33,14 @@ if (!started.ok) {
33
33
  return;
34
34
  }
35
35
  if (session.getSnapshot().status === "unselected") {
36
- const authorized = await session.authorize("choose");
36
+ const authorized = await session.authorize("choose", {
37
+ onFirstContact: ({ authenticationString }) => {
38
+ showPersistentSecurityPrompt({
39
+ title: "Compare with mdbase connect",
40
+ code: authenticationString
41
+ });
42
+ }
43
+ });
37
44
  if (!authorized.ok) {
38
45
  renderProblem(authorized.problem);
39
46
  return;
@@ -72,15 +79,15 @@ problem model, setup failures, mutation uncertainty, and UI guidance.
72
79
 
73
80
  `MdbaseConnect` is the application-level authorization registry.
74
81
  `MdbaseSession` is the normal application boundary: it owns active collection
75
- selection, authorization callback completion, access capabilities, and one
82
+ selection, authorization completion, access capabilities, and one
76
83
  reactive snapshot. Use `getSnapshot()` and `subscribe()` directly or through
77
84
  your framework's external-store integration. A snapshot is `unselected`,
78
85
  `ready`, or `unavailable`; an unavailable bookmark includes the explicit
79
86
  collection ID and reason instead of collapsing every recovery state to `null`.
80
87
 
81
88
  `MdbaseBrowserSelection` keeps the stable collection ID in
82
- `?collection=<id>`, preserves unrelated path/query/hash and router state,
83
- restores authorization callbacks safely, and reports browser back/forward
89
+ `?collection=<id>`, preserves unrelated path/query/hash and router state, and
90
+ reports browser back/forward
84
91
  changes to the session. The session auto-selects only when exactly one saved
85
92
  connection exists. Switching is state-driven and must not reload the page:
86
93
 
@@ -104,6 +111,16 @@ Collection IDs are opaque non-secret locators that may appear in browser
104
111
  history and logs. Grants remain the authorization boundary, and names are
105
112
  display text that may change.
106
113
 
114
+ When the chosen collection is owned by a computer that has not seen this
115
+ application installation before, `onFirstContact` is required. The SDK derives
116
+ the eight-character value locally from the application's non-extractable
117
+ installation key and the connector key; it never accepts a value supplied by
118
+ the control plane. Keep the value visible while the user compares and accepts
119
+ the exact same value in mdbase connect. Headless operators use
120
+ `mdbase connect trust list`, `trust show <id>`, and
121
+ `trust accept <id> --code XXXX-XXXX`. Hosted-only authorization does not invoke
122
+ this callback.
123
+
107
124
  Bundled v1 application manifests can declare runtime notification criteria.
108
125
  Register a service worker from a user gesture to receive standards-based Web
109
126
  Push while the app is closed:
@@ -378,12 +395,13 @@ Authorization is retained in `localStorage` by default. Access tokens are
378
395
  renewed with rotating refresh tokens; passing a custom `Storage` implementation
379
396
  allows a host to choose another persistence boundary.
380
397
 
381
- Native shells can pass `navigate` to open the authorization URL in the system
398
+ Native shells can pass `navigate` to open the approval URL in the system
382
399
  browser and list a reverse-domain callback such as
383
400
  `dev.mdbase.worklog://auth/mdbase/callback` in the bundled declaration. Its
384
- scheme must match the declaration ID. PKCE remains mandatory. Call
385
- `completeAuthorization(callbackUrl)` when the application receives the deep
386
- link.
401
+ scheme must match the declaration ID. The application remains alive, polls the
402
+ PKCE-bound status endpoint, displays first contact when required, and receives
403
+ the completed connection directly from `authorize()`; the browser does not
404
+ carry credentials back through a deep link.
387
405
 
388
406
  New authorizations require encrypted relay protocol 1 by default. The SDK keeps
389
407
  independent non-extractable P-256 ECDH agreement and ECDSA signing keys plus an
@@ -0,0 +1,5 @@
1
+ import { authorizationSigningMessage, type ApplicationAuthorizationBinding, type ApplicationAuthorizationProof } from "@mdbase-dev/connect-protocol";
2
+ import type { GrantKeyRecord } from "./crypto.js";
3
+ export { authorizationSigningMessage };
4
+ export declare function applicationInstallationId(identity: Pick<GrantKeyRecord, "agreementPublicKey" | "signingPublicKey">): Promise<string>;
5
+ export declare function signApplicationAuthorization(binding: ApplicationAuthorizationBinding, identity: Pick<GrantKeyRecord, "signingPrivateKey" | "agreementPublicKey" | "signingPublicKey">): Promise<ApplicationAuthorizationProof>;
@@ -0,0 +1,56 @@
1
+ import { applicationInstallationIdFromPublicKeys, authorizationSigningMessage } from "@mdbase-dev/connect-protocol";
2
+ const P256_ORDER = BigInt("0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551");
3
+ const P256_HALF_ORDER = P256_ORDER / 2n;
4
+ export { authorizationSigningMessage };
5
+ export async function applicationInstallationId(identity) {
6
+ return applicationInstallationIdFromPublicKeys(identity.agreementPublicKey, identity.signingPublicKey);
7
+ }
8
+ export async function signApplicationAuthorization(binding, identity) {
9
+ if (binding.installation_agreement_public_key !== identity.agreementPublicKey
10
+ || binding.installation_signing_public_key !== identity.signingPublicKey
11
+ || binding.application_installation_id !== await applicationInstallationId(identity)) {
12
+ throw new Error("Application authorization identity does not match its binding.");
13
+ }
14
+ const signature = canonicalSignature(new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, identity.signingPrivateKey, authorizationSigningMessage(binding))));
15
+ return {
16
+ binding,
17
+ signature: base64Url(signature)
18
+ };
19
+ }
20
+ function concat(values) {
21
+ const output = new Uint8Array(values.reduce((length, value) => length + value.byteLength, 0));
22
+ let offset = 0;
23
+ for (const value of values) {
24
+ output.set(value, offset);
25
+ offset += value.byteLength;
26
+ }
27
+ return output;
28
+ }
29
+ function base64Url(value) {
30
+ return btoa(String.fromCharCode(...value))
31
+ .replaceAll("+", "-")
32
+ .replaceAll("/", "_")
33
+ .replace(/=+$/u, "");
34
+ }
35
+ function canonicalSignature(signature) {
36
+ if (signature.byteLength !== 64) {
37
+ throw new Error("Application authorization signature is invalid.");
38
+ }
39
+ const s = bytesBigInt(signature.slice(32));
40
+ if (s === 0n || s >= P256_ORDER) {
41
+ throw new Error("Application authorization signature is invalid.");
42
+ }
43
+ if (s <= P256_HALF_ORDER)
44
+ return signature;
45
+ return concat([signature.slice(0, 32), bigIntBytes(P256_ORDER - s, 32)]);
46
+ }
47
+ function bytesBigInt(value) {
48
+ return BigInt(`0x${hex(value)}`);
49
+ }
50
+ function bigIntBytes(value, length) {
51
+ const encoded = value.toString(16).padStart(length * 2, "0");
52
+ return Uint8Array.from({ length }, (_, index) => Number.parseInt(encoded.slice(index * 2, index * 2 + 2), 16));
53
+ }
54
+ function hex(value) {
55
+ return [...value].map((byte) => byte.toString(16).padStart(2, "0")).join("");
56
+ }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "file": "mdbase-connect.min.js",
3
- "integrity": "sha384-5i/BVtYUAqTluHzqWtHkb2tI4mKjuOsQMdAKhtXjddN95BAkTPOpub7++MQX+fU0"
3
+ "integrity": "sha384-Rejwyx0zN9iv54K0UaKMWs09sQLXcxnOBtKT574KVKlC7aR4FZ7CqpPRMd7mKlNl"
4
4
  }