@actana/sdk 0.3.3 → 0.4.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.
@@ -0,0 +1,159 @@
1
+ // The client half of pairing that never leaves the machine: a key pair, and the
2
+ // certificate signing request that proves possession of it (#280, #284).
3
+ //
4
+ // A pairing exchange hands a Core a *public* key and gets a certificate back.
5
+ // This module is where the other half stays. Everything here runs before a
6
+ // socket is opened, and the only value that ever crosses one is
7
+ // {@link ClientCsr.csrPem} — the private key is returned to the caller, put
8
+ // into the resulting `CoreRegistrationBlob`, and never given to
9
+ // `core-pairing.ts` in a form it could serialise.
10
+ //
11
+ // **Why a hand-written PKCS#10 encoder rather than a library.** `node:crypto`
12
+ // generates key pairs and signs, and it exports a public key as a DER
13
+ // `SubjectPublicKeyInfo` — which is the only structure in a CSR that is hard to
14
+ // produce. What is left is four fields in a fixed order (RFC 2986 §4), so the
15
+ // choice was a ~90-line encoder against a new dependency on the one package in
16
+ // this repository whose dependency list is a reviewed artifact
17
+ // (`__tests__/package-boundaries.test.ts`, and ADR 0025's "a private one is a
18
+ // broken install"). The encoder is checked where it counts: the suite posts
19
+ // what it produces at the real Core, whose `assertSignableCsr` parses it with
20
+ // `@peculiar/x509` and verifies the signature before signing. A CSR this file
21
+ // got wrong fails that test rather than shipping.
22
+ //
23
+ // **RSA-2048, matching the Core.** `@actana/shared/core-cert-material` refuses
24
+ // anything under 2048 bits (`MIN_RSA_MODULUS_BITS`), and its own
25
+ // `generateClientCsr` mints exactly this. A client that asked for something
26
+ // more exotic would be betting on the CA's parser rather than on the algorithm
27
+ // the CA is known to sign.
28
+ import { generateKeyPair, sign } from "node:crypto";
29
+ /** Modulus size. See the header: what the Core's CA is known to sign. */
30
+ const RSA_MODULUS_BITS = 2048;
31
+ /** Longest common name written into the request. The Core caps its own at 48. */
32
+ const MAX_COMMON_NAME = 48;
33
+ /** The common name used when a caller offers nothing usable. */
34
+ const FALLBACK_COMMON_NAME = "actana-client";
35
+ /**
36
+ * Mint a key pair and a CSR naming `commonName`.
37
+ *
38
+ * The subject here is a *request*, and the Core does not honour it: the
39
+ * certificate is issued for the label the operator typed when they opened the
40
+ * pairing session (`core-pairing-routes.ts`, "the subject is the Core's to
41
+ * write"). It is filled in anyway because a CSR with an empty subject is a
42
+ * worse artifact to debug than one that says which machine asked.
43
+ */
44
+ export async function generateClientCsr(commonName) {
45
+ const { publicKey, privateKey } = await new Promise((resolve, reject) => {
46
+ generateKeyPair("rsa", { modulusLength: RSA_MODULUS_BITS }, (err, publicKey, privateKey) => {
47
+ if (err)
48
+ reject(err);
49
+ else
50
+ resolve({ publicKey, privateKey });
51
+ });
52
+ });
53
+ const spki = new Uint8Array(publicKey.export({ type: "spki", format: "der" }));
54
+ const info = derSequence(DER_VERSION_V1, derName(certificationRequestName(commonName)), spki,
55
+ // `attributes [0] IMPLICIT SET OF Attribute`, empty. Not optional in RFC
56
+ // 2986 — a request with the field left out is one some parsers reject —
57
+ // and nothing this client asks for belongs in it: requested extensions are
58
+ // exactly what the Core refuses to honour.
59
+ DER_EMPTY_ATTRIBUTES);
60
+ // RSASSA-PKCS1-v1_5 over SHA-256, which is what `sign` produces for an RSA
61
+ // key with no `padding` given, and what the algorithm identifier below says.
62
+ const signature = new Uint8Array(sign("sha256", info, privateKey));
63
+ const csr = derSequence(info, DER_SHA256_WITH_RSA, derBitString(signature));
64
+ return {
65
+ csrPem: derToPem("CERTIFICATE REQUEST", csr),
66
+ privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
67
+ };
68
+ }
69
+ /**
70
+ * A common name as it can appear in a distinguished name.
71
+ *
72
+ * Same conservative set the Core applies to the label it issues for, for the
73
+ * same reason: an X.509 name is structure, `,` and `=` and `+` are part of it,
74
+ * and a CSR is not the place to find out whose escaping is right.
75
+ */
76
+ function certificationRequestName(label) {
77
+ const cleaned = label.replace(/[^A-Za-z0-9 ._-]/g, "-").trim().slice(0, MAX_COMMON_NAME);
78
+ return cleaned.length > 0 ? cleaned : FALLBACK_COMMON_NAME;
79
+ }
80
+ // ─── A DER writer, as much of one as a CSR needs ───
81
+ //
82
+ // Tags are written as constants rather than an enum: there are five of them,
83
+ // each is used once, and a reader checking this file against RFC 2986 wants the
84
+ // byte.
85
+ /** `INTEGER 0` — `version` is v1, and v1 is zero. */
86
+ const DER_VERSION_V1 = Uint8Array.from([0x02, 0x01, 0x00]);
87
+ /** `[0]` constructed, zero-length: the empty attribute set. */
88
+ const DER_EMPTY_ATTRIBUTES = Uint8Array.from([0xa0, 0x00]);
89
+ /**
90
+ * `AlgorithmIdentifier` for `sha256WithRSAEncryption` — OID
91
+ * 1.2.840.113549.1.1.11, with the explicit `NULL` parameters RFC 4055 §5
92
+ * requires for it. Written out because it is a constant in every request this
93
+ * file produces; an OID encoder would be more code and one more thing to get
94
+ * wrong.
95
+ */
96
+ const DER_SHA256_WITH_RSA = Uint8Array.from([
97
+ 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00,
98
+ ]);
99
+ /** OID 2.5.4.3, `id-at-commonName`. */
100
+ const DER_OID_COMMON_NAME = Uint8Array.from([0x06, 0x03, 0x55, 0x04, 0x03]);
101
+ /** `SEQUENCE` of the given already-encoded members. */
102
+ function derSequence(...members) {
103
+ return derTagged(0x30, concat(members));
104
+ }
105
+ /** `RDNSequence` with the one relative distinguished name `CN=<value>`. */
106
+ function derName(commonName) {
107
+ const attribute = derSequence(DER_OID_COMMON_NAME, derUtf8String(commonName));
108
+ const rdn = derTagged(0x31, attribute); // SET
109
+ return derSequence(rdn);
110
+ }
111
+ function derUtf8String(value) {
112
+ return derTagged(0x0c, new TextEncoder().encode(value));
113
+ }
114
+ /**
115
+ * `BIT STRING` holding whole bytes.
116
+ *
117
+ * The leading zero is the count of unused bits in the final byte. A signature
118
+ * is a whole number of bytes, so it is always zero — and it is always present,
119
+ * because a `BIT STRING` without it is a malformed one.
120
+ */
121
+ function derBitString(bytes) {
122
+ return derTagged(0x03, concat([Uint8Array.from([0x00]), bytes]));
123
+ }
124
+ /** `tag | length | content`, with the length in DER's short or long form. */
125
+ function derTagged(tag, content) {
126
+ return concat([Uint8Array.from([tag]), derLength(content.length), content]);
127
+ }
128
+ /**
129
+ * DER's definite length.
130
+ *
131
+ * Under 128 the length is the byte. At or above it, the first byte is `0x80`
132
+ * plus the number of bytes that follow, big-endian and with no leading zero —
133
+ * the "minimal number of octets" DER insists on, which is why this counts the
134
+ * bytes rather than always writing four.
135
+ */
136
+ function derLength(length) {
137
+ if (length < 0x80)
138
+ return Uint8Array.from([length]);
139
+ const bytes = [];
140
+ for (let rest = length; rest > 0; rest = Math.floor(rest / 256))
141
+ bytes.unshift(rest % 256);
142
+ return Uint8Array.from([0x80 | bytes.length, ...bytes]);
143
+ }
144
+ function concat(parts) {
145
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
146
+ const out = new Uint8Array(total);
147
+ let offset = 0;
148
+ for (const part of parts) {
149
+ out.set(part, offset);
150
+ offset += part.length;
151
+ }
152
+ return out;
153
+ }
154
+ /** DER to PEM: base64 in 64-column lines between the two armour lines. */
155
+ function derToPem(label, der) {
156
+ const body = Buffer.from(der).toString("base64").replace(/(.{64})/g, "$1\n").trimEnd();
157
+ return `-----BEGIN ${label}-----\n${body}\n-----END ${label}-----\n`;
158
+ }
159
+ //# sourceMappingURL=core-pairing-csr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-pairing-csr.js","sourceRoot":"","sources":["../src/core-pairing-csr.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /** The one route a client with no certificate may reach on a Core. */
2
+ export declare const CORE_PAIRING_REDEEM_PATH = "/v1/pair/redeem";
3
+ /**
4
+ * What the client says about itself.
5
+ *
6
+ * The Core keeps the label and ignores the rest. "Ignores" is the honest word
7
+ * and is checked by `parseRedeemRequest`: an optional field a server drops is a
8
+ * client courtesy, not a promise the server has broken.
9
+ */
10
+ export type CorePairingClientInfo = {
11
+ /** The machine's own name for itself, e.g. a hostname. */
12
+ label?: string;
13
+ /**
14
+ * `process.platform`, sent by the CLI and the Panel.
15
+ *
16
+ * The Core reads it off the wire and stores nothing: it is not on
17
+ * {@link https://github.com/actana/control/issues/280 #280}'s paired-client
18
+ * record and `actana pair ls` does not show it. Surfacing it means adding a
19
+ * persisted field, which is a ticket rather than a parser change — #306's
20
+ * review raised it and it is tracked there.
21
+ */
22
+ platform?: string;
23
+ };
24
+ /**
25
+ * The redemption request body.
26
+ *
27
+ * `sessionId` names the pairing session the code belongs to and is not
28
+ * optional: the Core hashes a candidate code together with the session id and
29
+ * refuses to search for a session a code might fit, which is what stops a code
30
+ * lifted from one session being replayed against another.
31
+ */
32
+ export type CorePairingRedeemRequest = {
33
+ sessionId: string;
34
+ code: string;
35
+ client: CorePairingClientInfo;
36
+ /** PEM `CERTIFICATE REQUEST`. The private half is not in this object. */
37
+ csr: string;
38
+ };
39
+ /**
40
+ * The 200 body.
41
+ *
42
+ * Four fields, and the absence of a fifth is the point: there is no key here,
43
+ * because the Core never had one. `pairWithCore` supplies the fifth from the
44
+ * key it generated locally.
45
+ */
46
+ export type CorePairingRedeemResponse = {
47
+ /** The `wss://host:port` core link to dial from now on. */
48
+ endpoint: string;
49
+ /** PEM CA certificate — the trust anchor for every later dial. */
50
+ caCert: string;
51
+ /** PEM client certificate, signed from the CSR just posted. */
52
+ clientCert: string;
53
+ /** The signed bearer for the `auth` frame. */
54
+ bearer: string;
55
+ };
56
+ /** A refusal body, as every non-200 answer from the pairing route is shaped. */
57
+ export type CorePairingRefusalBody = {
58
+ /** The Core's machine-readable reason, e.g. `pairing-refused`. */
59
+ code?: string;
60
+ /** The human-readable one. */
61
+ error?: string;
62
+ };
63
+ //# sourceMappingURL=core-pairing-wire.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-pairing-wire.d.ts","sourceRoot":"","sources":["../src/core-pairing-wire.ts"],"names":[],"mappings":"AAoBA,sEAAsE;AACtE,eAAO,MAAM,wBAAwB,oBAAoB,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,qBAAqB,CAAC;IAC9B,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,gFAAgF;AAChF,MAAM,MAAM,sBAAsB,GAAG;IACnC,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC"}
@@ -0,0 +1,22 @@
1
+ // The pairing route's wire contract, and nothing else.
2
+ //
3
+ // This file exists so that the redeem request and response have **one**
4
+ // definition rather than two structurally identical ones — [ADR 0025][adr] D3:
5
+ // *"There is exactly one definition of every frame type, and a mirror is never
6
+ // the answer."* Until #306's review it was a mirror: `core-pairing.ts` declared
7
+ // the shapes for the client and `packages/core/src/core-pairing-routes.ts`
8
+ // hand-built the same shapes for the server, and nothing compiled the two
9
+ // against each other. A mirror does not fail; it disagrees, at runtime, on a
10
+ // wire, between two processes that each believe they are correct.
11
+ //
12
+ // It is a separate module from `core-pairing.ts` because of what D2 requires of
13
+ // anything the Core imports: **no I/O, no transport and no imports of its
14
+ // own.** `core-pairing.ts` dials, hashes and reads certificates, so a Core
15
+ // importing it would be a Core dialling itself. This file has no imports, and
16
+ // that is a property to preserve rather than a coincidence — it is the reason
17
+ // D2's list is allowed to grow to include it.
18
+ //
19
+ // [adr]: ../../docs/adr/0025-the-protocol-ships-with-the-client.md
20
+ /** The one route a client with no certificate may reach on a Core. */
21
+ export const CORE_PAIRING_REDEEM_PATH = "/v1/pair/redeem";
22
+ //# sourceMappingURL=core-pairing-wire.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-pairing-wire.js","sourceRoot":"","sources":["../src/core-pairing-wire.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,wEAAwE;AACxE,+EAA+E;AAC/E,+EAA+E;AAC/E,gFAAgF;AAChF,2EAA2E;AAC3E,0EAA0E;AAC1E,6EAA6E;AAC7E,kEAAkE;AAClE,EAAE;AACF,gFAAgF;AAChF,0EAA0E;AAC1E,2EAA2E;AAC3E,8EAA8E;AAC9E,8EAA8E;AAC9E,8CAA8C;AAC9C,EAAE;AACF,mEAAmE;AAEnE,sEAAsE;AACtE,MAAM,CAAC,MAAM,wBAAwB,GAAG,iBAAiB,CAAC"}
@@ -0,0 +1,211 @@
1
+ import type { CoreRegistrationBlob } from "./core-registration-blob.ts";
2
+ export { CORE_PAIRING_REDEEM_PATH, type CorePairingClientInfo, type CorePairingRedeemRequest, type CorePairingRedeemResponse, type CorePairingRefusalBody, } from "./core-pairing-wire.ts";
3
+ /**
4
+ * Why a pairing attempt did not produce a blob.
5
+ *
6
+ * The list is what a caller can *act* on, and it stops where the Core's own
7
+ * answers stop. `refused` covers a wrong code, an expired session, one already
8
+ * redeemed and one whose attempts are spent, because the Core answers all four
9
+ * with one status and one body on purpose (`core-pairing-routes.ts`, "every
10
+ * refusal is the same refusal"): telling them apart on the wire would tell an
11
+ * attacker whether a session exists and whether their last guess was closer.
12
+ * A client that reported four different things would be inventing three of
13
+ * them. The distinction the operator needs is in the Core's audit log, which
14
+ * is where #282 put it deliberately.
15
+ */
16
+ export type CorePairingFailure =
17
+ /** The address could not be read as a Core's HTTPS or core-link address. */
18
+ "bad-address"
19
+ /** The code was not eight characters, or named no pairing session. */
20
+ | "bad-code"
21
+ /** The expected fingerprint was not a SHA-256 fingerprint. */
22
+ | "bad-fingerprint"
23
+ /** Nothing answered at that address, or the dial timed out. */
24
+ | "unreachable"
25
+ /** The Core presented a chain with no CA in it — nothing to pin. */
26
+ | "no-ca-presented"
27
+ /** No fingerprint was given to check against. The code was not sent. */
28
+ | "fingerprint-unconfirmed"
29
+ /** The presented CA is not the expected one. The code was not sent. */
30
+ | "fingerprint-mismatch"
31
+ /**
32
+ * The expected CA, on an address its certificate does not cover.
33
+ *
34
+ * Its own failure rather than a mismatch, because it is not an attack and
35
+ * saying so sends the operator hunting for one: a Core's server certificate
36
+ * covers the host it was set up for plus loopback, so reaching a Core over a
37
+ * second interface, a tunnel or a DNS name added later fails here with the
38
+ * fingerprint matching perfectly.
39
+ */
40
+ | "hostname-mismatch"
41
+ /** The expected CA, and a certificate that is expired or otherwise unusable. */
42
+ | "certificate-invalid"
43
+ /** Wrong, expired, already redeemed, or out of attempts — see above. */
44
+ | "refused"
45
+ /** Too many attempts from this client, too fast. Try again later. */
46
+ | "rate-limited"
47
+ /** The Core would not accept the request itself — a bug on this side. */
48
+ | "rejected"
49
+ /** There is no pairing endpoint on that Core. */
50
+ | "not-pairable"
51
+ /** The Core failed while handling the redemption. */
52
+ | "core-error"
53
+ /** A 200 that was not a redemption response. */
54
+ | "malformed-response";
55
+ /** Everything a failure knows beyond its {@link CorePairingFailure}. */
56
+ export type CorePairingErrorDetail = {
57
+ /** The HTTP status, when the failure came from one. */
58
+ status?: number;
59
+ /** `retry-after`, in seconds, on a `rate-limited` failure. */
60
+ retryAfterSeconds?: number;
61
+ /** The Core's own refusal code, when it sent one. */
62
+ coreCode?: string;
63
+ /** The fingerprint the caller was told to expect. */
64
+ expectedFingerprint?: string;
65
+ /** The fingerprint the Core actually presented. */
66
+ presentedFingerprint?: string;
67
+ /** The CA the Core presented, PEM — for a UI that wants to show it. */
68
+ presentedCaCert?: string;
69
+ /** The TLS or OpenSSL code behind a dial failure, e.g. `CERT_HAS_EXPIRED`. */
70
+ tlsCode?: string;
71
+ };
72
+ /**
73
+ * A pairing attempt that did not produce a blob.
74
+ *
75
+ * One class with a {@link CorePairingFailure} rather than a class per failure:
76
+ * the CLI (#285) and the Panel (#286) both switch on the reason to write a
77
+ * sentence, and a `switch` over a union is checked by the compiler where a
78
+ * chain of `instanceof` is not.
79
+ */
80
+ export declare class CorePairingError extends Error {
81
+ readonly name = "CorePairingError";
82
+ /** Which failure this is. Switch on it; do not read the message. */
83
+ readonly failure: CorePairingFailure;
84
+ /** Whatever the failure knows beyond its reason. */
85
+ readonly detail: CorePairingErrorDetail;
86
+ constructor(failure: CorePairingFailure, message: string, detail?: CorePairingErrorDetail, options?: {
87
+ cause?: unknown;
88
+ });
89
+ }
90
+ /** What a bootstrap dial learns about a Core before anything is trusted. */
91
+ export type CorePairingIdentity = {
92
+ /** SHA-256 over the CA's DER, colon-separated uppercase hex. */
93
+ fingerprint: string;
94
+ /** The PEM CA certificate that fingerprint is of. */
95
+ caCert: string;
96
+ /** The host that was dialled. */
97
+ host: string;
98
+ /** The port that was dialled. */
99
+ port: number;
100
+ /** `https://host:port` — where a redemption would be posted. */
101
+ httpsOrigin: string;
102
+ };
103
+ /** How long a dial or a redemption may take before it is called unreachable. */
104
+ export declare const DEFAULT_PAIRING_TIMEOUT_MS = 15000;
105
+ /**
106
+ * Dial a Core and report the CA it presents — **without a code to send**.
107
+ *
108
+ * This is the first-contact mode, and the reason it is a separate function
109
+ * rather than a flag is that it takes no code: a UI that shows the operator's
110
+ * fingerprint beside the Core's, and asks a human whether they match, cannot
111
+ * leak a secret it was never given. {@link pairWithCore} calls it too, so the
112
+ * fingerprint a caller confirms is computed by the same code that later
113
+ * enforces it.
114
+ *
115
+ * The dial is unverified, because at this point in the flow there is nothing to
116
+ * verify against — that is what the fingerprint the operator read out is for.
117
+ * Nothing is sent on this connection and it is closed as soon as the chain has
118
+ * been read.
119
+ */
120
+ export declare function fetchCorePairingIdentity(opts: {
121
+ /** `host:port`, `https://host:port` or `wss://host:port`. */
122
+ address: string;
123
+ /** Defaults to {@link DEFAULT_PAIRING_TIMEOUT_MS}. */
124
+ timeoutMs?: number;
125
+ }): Promise<CorePairingIdentity>;
126
+ export type PairWithCoreOptions = {
127
+ /** The Core's address: `host:port`, `https://host:port` or `wss://host:port`. */
128
+ address: string;
129
+ /**
130
+ * The pairing code the operator read out — `XXXX-XXXX`, in any case and with
131
+ * or without the hyphen.
132
+ *
133
+ * A code names a session, and the Core will not go looking for which one, so
134
+ * the session id has to travel with it. Either pass it as `sessionId`, or
135
+ * pass a single `<sessionId>:<XXXX-XXXX>` string here and this reads both out
136
+ * of it. Which of the two an operator is given is #280's to settle across
137
+ * `actana pair new` (#283) and `actana core pair` (#285); both forms parse
138
+ * here so that neither is a change to this module.
139
+ */
140
+ code: string;
141
+ /** The pairing session the code belongs to, when `code` does not carry it. */
142
+ sessionId?: string;
143
+ /**
144
+ * The CA fingerprint the operator read out, in any of the forms a human
145
+ * copies it in: colon-separated or not, upper or lower case.
146
+ *
147
+ * **Absent is not "skip the check".** With no fingerprint this function
148
+ * refuses with `fingerprint-unconfirmed` and reports the presented one in the
149
+ * error, having sent no code — see {@link fetchCorePairingIdentity}.
150
+ */
151
+ expectedCaFingerprint?: string | null;
152
+ /** What this machine calls itself, for the operator's `actana pair ls`. */
153
+ label?: string;
154
+ /** This machine's platform, e.g. `process.platform`. */
155
+ platform?: string;
156
+ /** Defaults to {@link DEFAULT_PAIRING_TIMEOUT_MS}, per connection. */
157
+ timeoutMs?: number;
158
+ };
159
+ /**
160
+ * Pair with a Core and return the credential it issued.
161
+ *
162
+ * The result is a {@link CoreRegistrationBlob} and nothing downstream can tell
163
+ * it from a hand-carried one: `coreConnectionFromBlob` unpacks it,
164
+ * `httpsBaseUrlFor` derives the HTTPS origin, the PEMs go into the mTLS
165
+ * handshake and the bearer into the `auth` frame — exactly as they do today.
166
+ * `clientKey` is the key generated on this machine a few lines above; it was
167
+ * never sent, and the Core has never seen it.
168
+ *
169
+ * Throws {@link CorePairingError} for everything that is not a blob.
170
+ */
171
+ export declare function pairWithCore(opts: PairWithCoreOptions): Promise<CoreRegistrationBlob>;
172
+ /** A pairing code, and the session it names. */
173
+ export type PairingTicket = {
174
+ sessionId: string;
175
+ code: string;
176
+ };
177
+ /**
178
+ * Read a ticket out of what a human typed.
179
+ *
180
+ * The code is checked for *shape* — eight alphanumerics, hyphens and spaces
181
+ * ignored — and not against the Core's alphabet. That is a deliberate stop:
182
+ * the alphabet is an internal of `packages/shared` (which this package may not
183
+ * import) and mirroring it here would be a copy free to drift, which ADR 0025
184
+ * D3 is about. What the shape check buys is worth having on its own: a code
185
+ * that could not be right whatever the alphabet is never spends one of the
186
+ * five attempts the operator's session has.
187
+ */
188
+ export declare function parsePairingTicket(input: string, sessionId?: string): PairingTicket;
189
+ /** A Core's address, in the two forms this module needs it. */
190
+ type CoreAddress = {
191
+ host: string;
192
+ port: number;
193
+ httpsOrigin: string;
194
+ };
195
+ /**
196
+ * Read `host:port`, `https://host:port` or `wss://host:port`.
197
+ *
198
+ * `ws://` and `http://` are refused rather than upgraded: there is no
199
+ * certificate on a plaintext dial, so there is no fingerprint to check, and
200
+ * pairing over one would be the silent unverified exchange this module exists
201
+ * to make impossible. A caller that meant the secure port should say so.
202
+ */
203
+ export declare function parseCoreAddress(address: string): CoreAddress;
204
+ /**
205
+ * A fingerprint as this module compares them: colon-separated uppercase hex,
206
+ * which is the form `actana pair new` prints and a human copies.
207
+ */
208
+ export declare function parseFingerprint(input: string): string;
209
+ /** SHA-256 over a certificate's DER, in the form {@link parseFingerprint} yields. */
210
+ export declare function fingerprintOf(der: Uint8Array): string;
211
+ //# sourceMappingURL=core-pairing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-pairing.d.ts","sourceRoot":"","sources":["../src/core-pairing.ts"],"names":[],"mappings":"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"}