@peers-app/peers-sdk 0.25.14 → 0.25.16
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/dist/context/user-context-identity.test.d.ts +1 -0
- package/dist/context/user-context-identity.test.js +220 -0
- package/dist/context/user-context.d.ts +5 -1
- package/dist/context/user-context.js +63 -8
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/system/identity-contract.d.ts +156 -0
- package/dist/contracts/system/identity-contract.js +172 -0
- package/dist/contracts/system/index.d.ts +1 -1
- package/dist/data/assistants.d.ts +4 -4
- package/dist/data/devices.d.ts +70 -4
- package/dist/data/devices.js +3 -0
- package/dist/data/group-members.d.ts +2 -2
- package/dist/data/groups.d.ts +2 -2
- package/dist/data/invites.d.ts +93 -25
- package/dist/data/persistent-vars.d.ts +8 -0
- package/dist/data/persistent-vars.js +16 -0
- package/dist/data/tool-access-approvals.d.ts +2 -2
- package/dist/data/user-permissions.d.ts +10 -2
- package/dist/data/user-permissions.js +35 -4
- package/dist/data/user-permissions.test.js +26 -0
- package/dist/data/users.d.ts +48 -4
- package/dist/data/users.js +61 -5
- package/dist/data/workflows.d.ts +1 -1
- package/dist/device/connection.d.ts +7 -1
- package/dist/device/connection.js +38 -6
- package/dist/device/connection.test.js +33 -4
- package/dist/device/device.d.ts +2 -2
- package/dist/device/device.js +4 -2
- package/dist/device/get-trust-level-fn.d.ts +1 -0
- package/dist/device/get-trust-level-fn.js +28 -4
- package/dist/device/get-trust-level-fn.test.js +95 -0
- package/dist/device-pairing/types.d.ts +26 -26
- package/dist/host-credentials.d.ts +78 -0
- package/dist/host-credentials.js +128 -0
- package/dist/host-credentials.test.d.ts +1 -0
- package/dist/host-credentials.test.js +88 -0
- package/dist/identity/account-email.d.ts +59 -0
- package/dist/identity/account-email.js +87 -0
- package/dist/identity/account-email.test.d.ts +1 -0
- package/dist/identity/account-email.test.js +59 -0
- package/dist/identity/anchor-document.d.ts +205 -0
- package/dist/identity/anchor-document.js +119 -0
- package/dist/identity/anchor-document.test.d.ts +1 -0
- package/dist/identity/anchor-document.test.js +16 -0
- package/dist/identity/anchor-resolution.d.ts +61 -0
- package/dist/identity/anchor-resolution.js +117 -0
- package/dist/identity/anchor-resolution.test.d.ts +1 -0
- package/dist/identity/anchor-resolution.test.js +130 -0
- package/dist/identity/key-change.d.ts +142 -0
- package/dist/identity/key-change.js +472 -0
- package/dist/identity/key-change.test.d.ts +1 -0
- package/dist/identity/key-change.test.js +811 -0
- package/dist/identity/key-contest.d.ts +91 -0
- package/dist/identity/key-contest.js +55 -0
- package/dist/identity/key-resolutions.d.ts +55 -0
- package/dist/identity/key-resolutions.js +42 -0
- package/dist/identity/passkey.d.ts +22 -0
- package/dist/identity/passkey.js +33 -0
- package/dist/identity/user-row-change.d.ts +45 -0
- package/dist/identity/user-row-change.js +81 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/invites/invite-messages.d.ts +512 -116
- package/dist/invites/invite-token.d.ts +114 -24
- package/dist/invites/invite-token.js +6 -0
- package/dist/keys.d.ts +7 -0
- package/dist/keys.js +20 -0
- package/dist/system-ids.d.ts +16 -0
- package/dist/system-ids.js +16 -0
- package/dist/types/workflow.d.ts +2 -2
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +43 -0
- package/dist/utils.test.js +23 -0
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ACCOUNT_EMAIL_UNBIND_PURPOSE = exports.ACCOUNT_EMAIL_STATUS_PURPOSE = exports.ACCOUNT_EMAIL_BIND_PURPOSE = void 0;
|
|
4
|
+
exports.signEmailBind = signEmailBind;
|
|
5
|
+
exports.verifyEmailBind = verifyEmailBind;
|
|
6
|
+
exports.signEmailStatus = signEmailStatus;
|
|
7
|
+
exports.verifyEmailStatus = verifyEmailStatus;
|
|
8
|
+
exports.signEmailUnbind = signEmailUnbind;
|
|
9
|
+
exports.verifyEmailUnbind = verifyEmailUnbind;
|
|
10
|
+
const keys_1 = require("../keys");
|
|
11
|
+
/** Purpose string the current key signs to bind a recovery email. */
|
|
12
|
+
exports.ACCOUNT_EMAIL_BIND_PURPOSE = "peers-account-email-bind";
|
|
13
|
+
/**
|
|
14
|
+
* Signs a request to bind `email` to `userId` with the key on record.
|
|
15
|
+
*
|
|
16
|
+
* @param statement The user, the mailbox, and when the request was made
|
|
17
|
+
* @param secretKey Secret half of the key on record
|
|
18
|
+
*/
|
|
19
|
+
function signEmailBind(statement, secretKey) {
|
|
20
|
+
return (0, keys_1.signObjectWithSecretKey)({
|
|
21
|
+
purpose: exports.ACCOUNT_EMAIL_BIND_PURPOSE,
|
|
22
|
+
userId: statement.userId,
|
|
23
|
+
email: statement.email,
|
|
24
|
+
issuedAt: statement.issuedAt,
|
|
25
|
+
}, secretKey);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Checks that `signed` is the record key's signature over a bind statement for this user
|
|
29
|
+
* and mailbox. A different signer, user, or email is not a bind.
|
|
30
|
+
*/
|
|
31
|
+
function verifyEmailBind(params) {
|
|
32
|
+
if (params.signed.publicKey !== params.recordPublicKey)
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
const contents = (0, keys_1.openSignedObject)(params.signed);
|
|
36
|
+
return (contents.purpose === exports.ACCOUNT_EMAIL_BIND_PURPOSE &&
|
|
37
|
+
contents.userId === params.userId &&
|
|
38
|
+
contents.email === params.email);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Purpose string the current key signs to read whether a recovery email is bound. */
|
|
45
|
+
exports.ACCOUNT_EMAIL_STATUS_PURPOSE = "peers-account-email-status";
|
|
46
|
+
/** Signs a request to read the recovery email bound to `userId`. */
|
|
47
|
+
function signEmailStatus(statement, secretKey) {
|
|
48
|
+
return (0, keys_1.signObjectWithSecretKey)({
|
|
49
|
+
purpose: exports.ACCOUNT_EMAIL_STATUS_PURPOSE,
|
|
50
|
+
userId: statement.userId,
|
|
51
|
+
issuedAt: statement.issuedAt,
|
|
52
|
+
}, secretKey);
|
|
53
|
+
}
|
|
54
|
+
/** Checks that `signed` is the record key asking for this user's recovery email. */
|
|
55
|
+
function verifyEmailStatus(params) {
|
|
56
|
+
if (params.signed.publicKey !== params.recordPublicKey)
|
|
57
|
+
return false;
|
|
58
|
+
try {
|
|
59
|
+
const contents = (0, keys_1.openSignedObject)(params.signed);
|
|
60
|
+
return contents.purpose === exports.ACCOUNT_EMAIL_STATUS_PURPOSE && contents.userId === params.userId;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Purpose string the current key signs to remove a recovery email. */
|
|
67
|
+
exports.ACCOUNT_EMAIL_UNBIND_PURPOSE = "peers-account-email-unbind";
|
|
68
|
+
/** Signs a request to remove the recovery email bound to `userId`. */
|
|
69
|
+
function signEmailUnbind(statement, secretKey) {
|
|
70
|
+
return (0, keys_1.signObjectWithSecretKey)({
|
|
71
|
+
purpose: exports.ACCOUNT_EMAIL_UNBIND_PURPOSE,
|
|
72
|
+
userId: statement.userId,
|
|
73
|
+
issuedAt: statement.issuedAt,
|
|
74
|
+
}, secretKey);
|
|
75
|
+
}
|
|
76
|
+
/** Checks that `signed` is the record key removing this user's recovery email. */
|
|
77
|
+
function verifyEmailUnbind(params) {
|
|
78
|
+
if (params.signed.publicKey !== params.recordPublicKey)
|
|
79
|
+
return false;
|
|
80
|
+
try {
|
|
81
|
+
const contents = (0, keys_1.openSignedObject)(params.signed);
|
|
82
|
+
return contents.purpose === exports.ACCOUNT_EMAIL_UNBIND_PURPOSE && contents.userId === params.userId;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const keys_1 = require("../keys");
|
|
4
|
+
const account_email_1 = require("./account-email");
|
|
5
|
+
describe("account email statements", () => {
|
|
6
|
+
const keys = (0, keys_1.newKeys)();
|
|
7
|
+
const userId = "00maccountemailstatements";
|
|
8
|
+
const issuedAt = new Date().toISOString();
|
|
9
|
+
const bind = (0, account_email_1.signEmailBind)({ purpose: "peers-account-email-bind", userId, email: "ada@example.com", issuedAt }, keys.secretKey);
|
|
10
|
+
const status = (0, account_email_1.signEmailStatus)({ purpose: "peers-account-email-status", userId, issuedAt }, keys.secretKey);
|
|
11
|
+
const unbind = (0, account_email_1.signEmailUnbind)({ purpose: "peers-account-email-unbind", userId, issuedAt }, keys.secretKey);
|
|
12
|
+
it("accepts each statement for the purpose it was signed for", () => {
|
|
13
|
+
expect((0, account_email_1.verifyEmailBind)({
|
|
14
|
+
signed: bind,
|
|
15
|
+
userId,
|
|
16
|
+
email: "ada@example.com",
|
|
17
|
+
recordPublicKey: keys.publicKey,
|
|
18
|
+
})).toBe(true);
|
|
19
|
+
expect((0, account_email_1.verifyEmailStatus)({ signed: status, userId, recordPublicKey: keys.publicKey })).toBe(true);
|
|
20
|
+
expect((0, account_email_1.verifyEmailUnbind)({ signed: unbind, userId, recordPublicKey: keys.publicKey })).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it("does not let a statement signed for one purpose stand in for another", () => {
|
|
23
|
+
// A captured read request must not be able to remove the mailbox or bind a new one.
|
|
24
|
+
expect((0, account_email_1.verifyEmailUnbind)({ signed: status, userId, recordPublicKey: keys.publicKey })).toBe(false);
|
|
25
|
+
expect((0, account_email_1.verifyEmailBind)({
|
|
26
|
+
signed: status,
|
|
27
|
+
userId,
|
|
28
|
+
email: "ada@example.com",
|
|
29
|
+
recordPublicKey: keys.publicKey,
|
|
30
|
+
})).toBe(false);
|
|
31
|
+
expect((0, account_email_1.verifyEmailStatus)({ signed: unbind, userId, recordPublicKey: keys.publicKey })).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
it("refuses a statement for another user, another mailbox, or from another key", () => {
|
|
34
|
+
expect((0, account_email_1.verifyEmailBind)({
|
|
35
|
+
signed: bind,
|
|
36
|
+
userId: "00maccountemailotheruser0",
|
|
37
|
+
email: "ada@example.com",
|
|
38
|
+
recordPublicKey: keys.publicKey,
|
|
39
|
+
})).toBe(false);
|
|
40
|
+
expect((0, account_email_1.verifyEmailBind)({
|
|
41
|
+
signed: bind,
|
|
42
|
+
userId,
|
|
43
|
+
email: "eve@example.com",
|
|
44
|
+
recordPublicKey: keys.publicKey,
|
|
45
|
+
})).toBe(false);
|
|
46
|
+
expect((0, account_email_1.verifyEmailUnbind)({ signed: unbind, userId, recordPublicKey: (0, keys_1.newKeys)().publicKey })).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
it("refuses a statement whose contents were changed after signing", () => {
|
|
49
|
+
const forged = {
|
|
50
|
+
...unbind,
|
|
51
|
+
contents: { ...unbind.contents, userId: "00maccountemailotheruser0" },
|
|
52
|
+
};
|
|
53
|
+
expect((0, account_email_1.verifyEmailUnbind)({
|
|
54
|
+
signed: forged,
|
|
55
|
+
userId: "00maccountemailotheruser0",
|
|
56
|
+
recordPublicKey: keys.publicKey,
|
|
57
|
+
})).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type ISignedObject } from "../keys";
|
|
3
|
+
/** Purpose string signed by the previous key when a succession is a rotation. */
|
|
4
|
+
export declare const KEY_SUCCESSION_PURPOSE: "peers-key-succession";
|
|
5
|
+
/** Anchor kinds the runtime knows how to fetch. */
|
|
6
|
+
export declare const ANCHOR_TYPES: readonly ["peers-services", "https"];
|
|
7
|
+
/** One of {@link ANCHOR_TYPES}. */
|
|
8
|
+
export type AnchorType = (typeof ANCHOR_TYPES)[number];
|
|
9
|
+
/** Why a key entry was published. Informational. */
|
|
10
|
+
export declare const ANCHOR_KEY_REASONS: readonly ["rotation", "compromised", "lost"];
|
|
11
|
+
/** One of {@link ANCHOR_KEY_REASONS}. */
|
|
12
|
+
export type AnchorKeyReason = (typeof ANCHOR_KEY_REASONS)[number];
|
|
13
|
+
/** A place that publishes a user's current key. */
|
|
14
|
+
export declare const anchorRefSchema: z.ZodObject<{
|
|
15
|
+
type: z.ZodEnum<["peers-services", "https"]>;
|
|
16
|
+
url: z.ZodString;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
type: "peers-services" | "https";
|
|
19
|
+
url: string;
|
|
20
|
+
}, {
|
|
21
|
+
type: "peers-services" | "https";
|
|
22
|
+
url: string;
|
|
23
|
+
}>;
|
|
24
|
+
/** A place that publishes a user's current key. */
|
|
25
|
+
export type IAnchorRef = z.infer<typeof anchorRefSchema>;
|
|
26
|
+
/** How many anchors one account may list. */
|
|
27
|
+
export declare const MAX_ANCHORS = 8;
|
|
28
|
+
/**
|
|
29
|
+
* Checks and orders an anchor list.
|
|
30
|
+
* Duplicates are dropped. An `https` anchor must be an https URL.
|
|
31
|
+
* A `peers-services` anchor may be http, which is how a local registry is addressed.
|
|
32
|
+
* An empty list is an opt-out.
|
|
33
|
+
*
|
|
34
|
+
* @param input The list a caller wants to publish
|
|
35
|
+
*/
|
|
36
|
+
export declare function normalizeAnchorList(input: unknown): IAnchorRef[];
|
|
37
|
+
/**
|
|
38
|
+
* Statement the previous key signs so peers can tell rotation from recovery.
|
|
39
|
+
* `publicKey` is the new key.
|
|
40
|
+
*/
|
|
41
|
+
export declare const keySuccessionStatementSchema: z.ZodObject<{
|
|
42
|
+
purpose: z.ZodLiteral<"peers-key-succession">;
|
|
43
|
+
userId: z.ZodEffects<z.ZodString, string, string>;
|
|
44
|
+
publicKey: z.ZodString;
|
|
45
|
+
issuedAt: z.ZodString;
|
|
46
|
+
}, "strip", z.ZodTypeAny, {
|
|
47
|
+
publicKey: string;
|
|
48
|
+
purpose: "peers-key-succession";
|
|
49
|
+
userId: string;
|
|
50
|
+
issuedAt: string;
|
|
51
|
+
}, {
|
|
52
|
+
publicKey: string;
|
|
53
|
+
purpose: "peers-key-succession";
|
|
54
|
+
userId: string;
|
|
55
|
+
issuedAt: string;
|
|
56
|
+
}>;
|
|
57
|
+
/** Statement the previous key signs so peers can tell rotation from recovery. */
|
|
58
|
+
export type IKeySuccessionStatement = z.infer<typeof keySuccessionStatementSchema>;
|
|
59
|
+
/** Previous key's co-signature over a {@link IKeySuccessionStatement}. */
|
|
60
|
+
export declare const anchorAuthorizedBySchema: z.ZodObject<{
|
|
61
|
+
publicKey: z.ZodString;
|
|
62
|
+
signature: z.ZodString;
|
|
63
|
+
}, "strip", z.ZodTypeAny, {
|
|
64
|
+
publicKey: string;
|
|
65
|
+
signature: string;
|
|
66
|
+
}, {
|
|
67
|
+
publicKey: string;
|
|
68
|
+
signature: string;
|
|
69
|
+
}>;
|
|
70
|
+
/** Previous key's co-signature over a succession statement. */
|
|
71
|
+
export type IAnchorAuthorizedBy = z.infer<typeof anchorAuthorizedBySchema>;
|
|
72
|
+
/** One key an anchor currently publishes. */
|
|
73
|
+
export declare const anchorKeyEntrySchema: z.ZodObject<{
|
|
74
|
+
publicKey: z.ZodString;
|
|
75
|
+
issuedAt: z.ZodString;
|
|
76
|
+
reason: z.ZodOptional<z.ZodEnum<["rotation", "compromised", "lost"]>>;
|
|
77
|
+
authorizedBy: z.ZodOptional<z.ZodObject<{
|
|
78
|
+
publicKey: z.ZodString;
|
|
79
|
+
signature: z.ZodString;
|
|
80
|
+
}, "strip", z.ZodTypeAny, {
|
|
81
|
+
publicKey: string;
|
|
82
|
+
signature: string;
|
|
83
|
+
}, {
|
|
84
|
+
publicKey: string;
|
|
85
|
+
signature: string;
|
|
86
|
+
}>>;
|
|
87
|
+
}, "strip", z.ZodTypeAny, {
|
|
88
|
+
publicKey: string;
|
|
89
|
+
issuedAt: string;
|
|
90
|
+
reason?: "rotation" | "compromised" | "lost" | undefined;
|
|
91
|
+
authorizedBy?: {
|
|
92
|
+
publicKey: string;
|
|
93
|
+
signature: string;
|
|
94
|
+
} | undefined;
|
|
95
|
+
}, {
|
|
96
|
+
publicKey: string;
|
|
97
|
+
issuedAt: string;
|
|
98
|
+
reason?: "rotation" | "compromised" | "lost" | undefined;
|
|
99
|
+
authorizedBy?: {
|
|
100
|
+
publicKey: string;
|
|
101
|
+
signature: string;
|
|
102
|
+
} | undefined;
|
|
103
|
+
}>;
|
|
104
|
+
/** One key an anchor currently publishes. */
|
|
105
|
+
export type IAnchorKeyEntry = z.infer<typeof anchorKeyEntrySchema>;
|
|
106
|
+
/** Document served by every anchor, peers-services and self-hosted alike. */
|
|
107
|
+
export declare const anchorDocumentSchema: z.ZodObject<{
|
|
108
|
+
version: z.ZodLiteral<1>;
|
|
109
|
+
userId: z.ZodEffects<z.ZodString, string, string>;
|
|
110
|
+
keys: z.ZodArray<z.ZodObject<{
|
|
111
|
+
publicKey: z.ZodString;
|
|
112
|
+
issuedAt: z.ZodString;
|
|
113
|
+
reason: z.ZodOptional<z.ZodEnum<["rotation", "compromised", "lost"]>>;
|
|
114
|
+
authorizedBy: z.ZodOptional<z.ZodObject<{
|
|
115
|
+
publicKey: z.ZodString;
|
|
116
|
+
signature: z.ZodString;
|
|
117
|
+
}, "strip", z.ZodTypeAny, {
|
|
118
|
+
publicKey: string;
|
|
119
|
+
signature: string;
|
|
120
|
+
}, {
|
|
121
|
+
publicKey: string;
|
|
122
|
+
signature: string;
|
|
123
|
+
}>>;
|
|
124
|
+
}, "strip", z.ZodTypeAny, {
|
|
125
|
+
publicKey: string;
|
|
126
|
+
issuedAt: string;
|
|
127
|
+
reason?: "rotation" | "compromised" | "lost" | undefined;
|
|
128
|
+
authorizedBy?: {
|
|
129
|
+
publicKey: string;
|
|
130
|
+
signature: string;
|
|
131
|
+
} | undefined;
|
|
132
|
+
}, {
|
|
133
|
+
publicKey: string;
|
|
134
|
+
issuedAt: string;
|
|
135
|
+
reason?: "rotation" | "compromised" | "lost" | undefined;
|
|
136
|
+
authorizedBy?: {
|
|
137
|
+
publicKey: string;
|
|
138
|
+
signature: string;
|
|
139
|
+
} | undefined;
|
|
140
|
+
}>, "many">;
|
|
141
|
+
anchors: z.ZodArray<z.ZodObject<{
|
|
142
|
+
type: z.ZodEnum<["peers-services", "https"]>;
|
|
143
|
+
url: z.ZodString;
|
|
144
|
+
}, "strip", z.ZodTypeAny, {
|
|
145
|
+
type: "peers-services" | "https";
|
|
146
|
+
url: string;
|
|
147
|
+
}, {
|
|
148
|
+
type: "peers-services" | "https";
|
|
149
|
+
url: string;
|
|
150
|
+
}>, "many">;
|
|
151
|
+
updatedAt: z.ZodString;
|
|
152
|
+
}, "strip", z.ZodTypeAny, {
|
|
153
|
+
keys: {
|
|
154
|
+
publicKey: string;
|
|
155
|
+
issuedAt: string;
|
|
156
|
+
reason?: "rotation" | "compromised" | "lost" | undefined;
|
|
157
|
+
authorizedBy?: {
|
|
158
|
+
publicKey: string;
|
|
159
|
+
signature: string;
|
|
160
|
+
} | undefined;
|
|
161
|
+
}[];
|
|
162
|
+
version: 1;
|
|
163
|
+
userId: string;
|
|
164
|
+
anchors: {
|
|
165
|
+
type: "peers-services" | "https";
|
|
166
|
+
url: string;
|
|
167
|
+
}[];
|
|
168
|
+
updatedAt: string;
|
|
169
|
+
}, {
|
|
170
|
+
keys: {
|
|
171
|
+
publicKey: string;
|
|
172
|
+
issuedAt: string;
|
|
173
|
+
reason?: "rotation" | "compromised" | "lost" | undefined;
|
|
174
|
+
authorizedBy?: {
|
|
175
|
+
publicKey: string;
|
|
176
|
+
signature: string;
|
|
177
|
+
} | undefined;
|
|
178
|
+
}[];
|
|
179
|
+
version: 1;
|
|
180
|
+
userId: string;
|
|
181
|
+
anchors: {
|
|
182
|
+
type: "peers-services" | "https";
|
|
183
|
+
url: string;
|
|
184
|
+
}[];
|
|
185
|
+
updatedAt: string;
|
|
186
|
+
}>;
|
|
187
|
+
/** Document served by every anchor, peers-services and self-hosted alike. */
|
|
188
|
+
export type IAnchorDocument = z.infer<typeof anchorDocumentSchema>;
|
|
189
|
+
/**
|
|
190
|
+
* Signs a succession statement with the previous key.
|
|
191
|
+
*
|
|
192
|
+
* @param statement The new key, the user, and when it was issued
|
|
193
|
+
* @param previousSecretKey Secret half of the key on record
|
|
194
|
+
*/
|
|
195
|
+
export declare function signSuccession(statement: IKeySuccessionStatement, previousSecretKey: string): ISignedObject<IKeySuccessionStatement>;
|
|
196
|
+
/**
|
|
197
|
+
* Checks that `authorizedBy` is a signature by `recordPublicKey` over the succession
|
|
198
|
+
* statement for this key entry. A mismatch, a bad signature, or a different signer
|
|
199
|
+
* is not a rotation.
|
|
200
|
+
*/
|
|
201
|
+
export declare function verifySuccession(params: {
|
|
202
|
+
userId: string;
|
|
203
|
+
recordPublicKey: string;
|
|
204
|
+
entry: IAnchorKeyEntry;
|
|
205
|
+
}): boolean;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.anchorDocumentSchema = exports.anchorKeyEntrySchema = exports.anchorAuthorizedBySchema = exports.keySuccessionStatementSchema = exports.MAX_ANCHORS = exports.anchorRefSchema = exports.ANCHOR_KEY_REASONS = exports.ANCHOR_TYPES = exports.KEY_SUCCESSION_PURPOSE = void 0;
|
|
4
|
+
exports.normalizeAnchorList = normalizeAnchorList;
|
|
5
|
+
exports.signSuccession = signSuccession;
|
|
6
|
+
exports.verifySuccession = verifySuccession;
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const keys_1 = require("../keys");
|
|
9
|
+
const zod_types_1 = require("../types/zod-types");
|
|
10
|
+
/** Purpose string signed by the previous key when a succession is a rotation. */
|
|
11
|
+
exports.KEY_SUCCESSION_PURPOSE = "peers-key-succession";
|
|
12
|
+
/** Anchor kinds the runtime knows how to fetch. */
|
|
13
|
+
exports.ANCHOR_TYPES = ["peers-services", "https"];
|
|
14
|
+
/** Why a key entry was published. Informational. */
|
|
15
|
+
exports.ANCHOR_KEY_REASONS = ["rotation", "compromised", "lost"];
|
|
16
|
+
/** A place that publishes a user's current key. */
|
|
17
|
+
exports.anchorRefSchema = zod_1.z.object({
|
|
18
|
+
type: zod_1.z.enum(exports.ANCHOR_TYPES),
|
|
19
|
+
url: zod_1.z.string().url(),
|
|
20
|
+
});
|
|
21
|
+
/** How many anchors one account may list. */
|
|
22
|
+
exports.MAX_ANCHORS = 8;
|
|
23
|
+
/**
|
|
24
|
+
* Checks and orders an anchor list.
|
|
25
|
+
* Duplicates are dropped. An `https` anchor must be an https URL.
|
|
26
|
+
* A `peers-services` anchor may be http, which is how a local registry is addressed.
|
|
27
|
+
* An empty list is an opt-out.
|
|
28
|
+
*
|
|
29
|
+
* @param input The list a caller wants to publish
|
|
30
|
+
*/
|
|
31
|
+
function normalizeAnchorList(input) {
|
|
32
|
+
const list = zod_1.z.array(exports.anchorRefSchema).max(exports.MAX_ANCHORS).parse(input);
|
|
33
|
+
for (const anchor of list) {
|
|
34
|
+
const url = new URL(anchor.url);
|
|
35
|
+
if (anchor.type === "https" && url.protocol !== "https:") {
|
|
36
|
+
throw new Error("Anchor URLs must use https");
|
|
37
|
+
}
|
|
38
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
39
|
+
throw new Error("Anchor URLs must use http or https");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const unique = list.filter((anchor) => {
|
|
44
|
+
const key = `${anchor.type} ${anchor.url}`;
|
|
45
|
+
if (seen.has(key))
|
|
46
|
+
return false;
|
|
47
|
+
seen.add(key);
|
|
48
|
+
return true;
|
|
49
|
+
});
|
|
50
|
+
unique.sort((a, b) => `${a.type} ${a.url}`.localeCompare(`${b.type} ${b.url}`));
|
|
51
|
+
return unique;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Statement the previous key signs so peers can tell rotation from recovery.
|
|
55
|
+
* `publicKey` is the new key.
|
|
56
|
+
*/
|
|
57
|
+
exports.keySuccessionStatementSchema = zod_1.z.object({
|
|
58
|
+
purpose: zod_1.z.literal(exports.KEY_SUCCESSION_PURPOSE),
|
|
59
|
+
userId: zod_types_1.zodPeerId,
|
|
60
|
+
publicKey: zod_1.z.string().min(1),
|
|
61
|
+
issuedAt: zod_1.z.string().min(1),
|
|
62
|
+
});
|
|
63
|
+
/** Previous key's co-signature over a {@link IKeySuccessionStatement}. */
|
|
64
|
+
exports.anchorAuthorizedBySchema = zod_1.z.object({
|
|
65
|
+
publicKey: zod_1.z.string().min(1),
|
|
66
|
+
signature: zod_1.z.string().min(1),
|
|
67
|
+
});
|
|
68
|
+
/** One key an anchor currently publishes. */
|
|
69
|
+
exports.anchorKeyEntrySchema = zod_1.z.object({
|
|
70
|
+
publicKey: zod_1.z.string().min(1),
|
|
71
|
+
issuedAt: zod_1.z.string().min(1),
|
|
72
|
+
reason: zod_1.z.enum(exports.ANCHOR_KEY_REASONS).optional(),
|
|
73
|
+
authorizedBy: exports.anchorAuthorizedBySchema.optional(),
|
|
74
|
+
});
|
|
75
|
+
/** Document served by every anchor, peers-services and self-hosted alike. */
|
|
76
|
+
exports.anchorDocumentSchema = zod_1.z.object({
|
|
77
|
+
version: zod_1.z.literal(1),
|
|
78
|
+
userId: zod_types_1.zodPeerId,
|
|
79
|
+
keys: zod_1.z.array(exports.anchorKeyEntrySchema),
|
|
80
|
+
anchors: zod_1.z.array(exports.anchorRefSchema),
|
|
81
|
+
updatedAt: zod_1.z.string().min(1),
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* Signs a succession statement with the previous key.
|
|
85
|
+
*
|
|
86
|
+
* @param statement The new key, the user, and when it was issued
|
|
87
|
+
* @param previousSecretKey Secret half of the key on record
|
|
88
|
+
*/
|
|
89
|
+
function signSuccession(statement, previousSecretKey) {
|
|
90
|
+
const parsed = exports.keySuccessionStatementSchema.parse(statement);
|
|
91
|
+
return (0, keys_1.signObjectWithSecretKey)(parsed, previousSecretKey);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Checks that `authorizedBy` is a signature by `recordPublicKey` over the succession
|
|
95
|
+
* statement for this key entry. A mismatch, a bad signature, or a different signer
|
|
96
|
+
* is not a rotation.
|
|
97
|
+
*/
|
|
98
|
+
function verifySuccession(params) {
|
|
99
|
+
const { authorizedBy, publicKey, issuedAt } = params.entry;
|
|
100
|
+
if (!authorizedBy || authorizedBy.publicKey !== params.recordPublicKey)
|
|
101
|
+
return false;
|
|
102
|
+
try {
|
|
103
|
+
const contents = (0, keys_1.openSignedObject)({
|
|
104
|
+
contents: {
|
|
105
|
+
purpose: exports.KEY_SUCCESSION_PURPOSE,
|
|
106
|
+
userId: params.userId,
|
|
107
|
+
publicKey,
|
|
108
|
+
issuedAt,
|
|
109
|
+
},
|
|
110
|
+
signature: authorizedBy.signature,
|
|
111
|
+
publicKey: authorizedBy.publicKey,
|
|
112
|
+
});
|
|
113
|
+
const parsed = exports.keySuccessionStatementSchema.safeParse(contents);
|
|
114
|
+
return (parsed.success && parsed.data.publicKey === publicKey && parsed.data.userId === params.userId);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const anchor_document_1 = require("./anchor-document");
|
|
4
|
+
describe("normalizeAnchorList", () => {
|
|
5
|
+
it("allows http for peers-services and requires https for a page anchor", () => {
|
|
6
|
+
const list = (0, anchor_document_1.normalizeAnchorList)([
|
|
7
|
+
{ type: "peers-services", url: "http://127.0.0.1:4000/api/v1/keys/abc" },
|
|
8
|
+
{ type: "https", url: "https://example.com/peers.json" },
|
|
9
|
+
]);
|
|
10
|
+
expect(list.map((anchor) => anchor.url)).toEqual([
|
|
11
|
+
"https://example.com/peers.json",
|
|
12
|
+
"http://127.0.0.1:4000/api/v1/keys/abc",
|
|
13
|
+
]);
|
|
14
|
+
expect(() => (0, anchor_document_1.normalizeAnchorList)([{ type: "https", url: "http://example.com/peers.json" }])).toThrow(/https/);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type IAnchorDocument, type IAnchorRef } from "./anchor-document";
|
|
2
|
+
/** Whether two anchor lists name the same places, ignoring order. */
|
|
3
|
+
export declare function sameAnchorList(left: IAnchorRef[] | undefined, right: IAnchorRef[] | undefined): boolean;
|
|
4
|
+
/** What the anchors on record say about a proposed replacement list. */
|
|
5
|
+
export type AnchorListDecision = "confirmed" | "pending" | "vetoed";
|
|
6
|
+
/**
|
|
7
|
+
* Applies the confirmation rule to a new anchor list.
|
|
8
|
+
* A document whose `anchors` match the proposal confirms. A different list vetoes.
|
|
9
|
+
* No answer is pending.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolvePresentedAnchors(params: {
|
|
12
|
+
userId: string;
|
|
13
|
+
presented: IAnchorRef[];
|
|
14
|
+
outcomes: AnchorFetchOutcome[];
|
|
15
|
+
}): AnchorListDecision;
|
|
16
|
+
/**
|
|
17
|
+
* How long a resolution may be reused before the anchors are fetched again.
|
|
18
|
+
* Matches the peers-services key document `max-age`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const ANCHOR_CACHE_MS = 60000;
|
|
21
|
+
/** What one anchor fetch returned. Network failure is an abstain, not a veto. */
|
|
22
|
+
export type AnchorFetchOutcome = {
|
|
23
|
+
outcome: "document";
|
|
24
|
+
document: IAnchorDocument;
|
|
25
|
+
maxAgeSeconds?: number;
|
|
26
|
+
} | {
|
|
27
|
+
outcome: "veto";
|
|
28
|
+
} | {
|
|
29
|
+
outcome: "abstain";
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Fetches one anchor. Installed by the host. Should not throw; a failure is an abstain.
|
|
33
|
+
*/
|
|
34
|
+
export type AnchorFetcher = (anchor: IAnchorRef, userId: string) => Promise<AnchorFetchOutcome>;
|
|
35
|
+
/** Installs the fetcher {@link import("./key-change").evaluatePresentedKey} uses. */
|
|
36
|
+
export declare function setAnchorFetcher(fetcher: AnchorFetcher | undefined): void;
|
|
37
|
+
/** The installed fetcher, if any. */
|
|
38
|
+
export declare function getAnchorFetcher(): AnchorFetcher | undefined;
|
|
39
|
+
/** Stored resolution state. `confirmed-recovery` waits for the recovery delay. */
|
|
40
|
+
export declare const KEY_RESOLUTION_STATES: readonly ["pending", "vetoed", "confirmed-rotation", "confirmed-recovery"];
|
|
41
|
+
/** One of {@link KEY_RESOLUTION_STATES}. */
|
|
42
|
+
export type KeyResolutionState = (typeof KEY_RESOLUTION_STATES)[number];
|
|
43
|
+
/** Result of applying the confirmation rule to a set of anchor answers. */
|
|
44
|
+
export interface AnchorResolution {
|
|
45
|
+
state: KeyResolutionState;
|
|
46
|
+
/** True when a confirming anchor carried the key on record's co-signature. */
|
|
47
|
+
successionByRecordKey: boolean;
|
|
48
|
+
/** Cache lifetime, in seconds, taken from the shortest document `max-age` that answered. */
|
|
49
|
+
cacheSeconds: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Applies the confirmation rule: at least one anchor lists the presented key, and none veto.
|
|
53
|
+
* A co-signature by the key on record makes it a rotation. Anything else confirmed is a recovery.
|
|
54
|
+
* No answer is pending. Any veto refuses the presented key.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolvePresentedKey(params: {
|
|
57
|
+
userId: string;
|
|
58
|
+
recordPublicKey: string;
|
|
59
|
+
presentedPublicKey: string;
|
|
60
|
+
outcomes: AnchorFetchOutcome[];
|
|
61
|
+
}): AnchorResolution;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KEY_RESOLUTION_STATES = exports.ANCHOR_CACHE_MS = void 0;
|
|
4
|
+
exports.sameAnchorList = sameAnchorList;
|
|
5
|
+
exports.resolvePresentedAnchors = resolvePresentedAnchors;
|
|
6
|
+
exports.setAnchorFetcher = setAnchorFetcher;
|
|
7
|
+
exports.getAnchorFetcher = getAnchorFetcher;
|
|
8
|
+
exports.resolvePresentedKey = resolvePresentedKey;
|
|
9
|
+
const anchor_document_1 = require("./anchor-document");
|
|
10
|
+
/** Whether two anchor lists name the same places, ignoring order. */
|
|
11
|
+
function sameAnchorList(left, right) {
|
|
12
|
+
const a = [...(left ?? [])].sort((x, y) => `${x.type} ${x.url}`.localeCompare(`${y.type} ${y.url}`));
|
|
13
|
+
const b = [...(right ?? [])].sort((x, y) => `${x.type} ${x.url}`.localeCompare(`${y.type} ${y.url}`));
|
|
14
|
+
if (a.length !== b.length)
|
|
15
|
+
return false;
|
|
16
|
+
return a.every((anchor, index) => anchor.type === b[index].type && anchor.url === b[index].url);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Applies the confirmation rule to a new anchor list.
|
|
20
|
+
* A document whose `anchors` match the proposal confirms. A different list vetoes.
|
|
21
|
+
* No answer is pending.
|
|
22
|
+
*/
|
|
23
|
+
function resolvePresentedAnchors(params) {
|
|
24
|
+
let confirms = 0;
|
|
25
|
+
let vetoes = 0;
|
|
26
|
+
for (const outcome of params.outcomes) {
|
|
27
|
+
if (outcome.outcome === "abstain")
|
|
28
|
+
continue;
|
|
29
|
+
if (outcome.outcome === "veto") {
|
|
30
|
+
vetoes++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (outcome.document.userId !== params.userId ||
|
|
34
|
+
!sameAnchorList(outcome.document.anchors, params.presented)) {
|
|
35
|
+
vetoes++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
confirms++;
|
|
39
|
+
}
|
|
40
|
+
if (vetoes > 0)
|
|
41
|
+
return "vetoed";
|
|
42
|
+
if (confirms === 0)
|
|
43
|
+
return "pending";
|
|
44
|
+
return "confirmed";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* How long a resolution may be reused before the anchors are fetched again.
|
|
48
|
+
* Matches the peers-services key document `max-age`.
|
|
49
|
+
*/
|
|
50
|
+
exports.ANCHOR_CACHE_MS = 60_000;
|
|
51
|
+
let anchorFetcher;
|
|
52
|
+
/** Installs the fetcher {@link import("./key-change").evaluatePresentedKey} uses. */
|
|
53
|
+
function setAnchorFetcher(fetcher) {
|
|
54
|
+
anchorFetcher = fetcher;
|
|
55
|
+
}
|
|
56
|
+
/** The installed fetcher, if any. */
|
|
57
|
+
function getAnchorFetcher() {
|
|
58
|
+
return anchorFetcher;
|
|
59
|
+
}
|
|
60
|
+
/** Stored resolution state. `confirmed-recovery` waits for the recovery delay. */
|
|
61
|
+
exports.KEY_RESOLUTION_STATES = [
|
|
62
|
+
"pending",
|
|
63
|
+
"vetoed",
|
|
64
|
+
"confirmed-rotation",
|
|
65
|
+
"confirmed-recovery",
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* Applies the confirmation rule: at least one anchor lists the presented key, and none veto.
|
|
69
|
+
* A co-signature by the key on record makes it a rotation. Anything else confirmed is a recovery.
|
|
70
|
+
* No answer is pending. Any veto refuses the presented key.
|
|
71
|
+
*/
|
|
72
|
+
function resolvePresentedKey(params) {
|
|
73
|
+
let confirms = 0;
|
|
74
|
+
let vetoes = 0;
|
|
75
|
+
let succession = false;
|
|
76
|
+
let cacheSeconds = exports.ANCHOR_CACHE_MS / 1000;
|
|
77
|
+
for (const outcome of params.outcomes) {
|
|
78
|
+
if (outcome.outcome === "abstain")
|
|
79
|
+
continue;
|
|
80
|
+
if (outcome.outcome === "veto") {
|
|
81
|
+
vetoes++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (typeof outcome.maxAgeSeconds === "number" && outcome.maxAgeSeconds >= 0) {
|
|
85
|
+
cacheSeconds = Math.min(cacheSeconds, outcome.maxAgeSeconds);
|
|
86
|
+
}
|
|
87
|
+
const document = outcome.document;
|
|
88
|
+
if (document.userId !== params.userId) {
|
|
89
|
+
vetoes++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const entry = document.keys.find((key) => key.publicKey === params.presentedPublicKey);
|
|
93
|
+
if (!entry) {
|
|
94
|
+
vetoes++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
confirms++;
|
|
98
|
+
if ((0, anchor_document_1.verifySuccession)({
|
|
99
|
+
userId: params.userId,
|
|
100
|
+
recordPublicKey: params.recordPublicKey,
|
|
101
|
+
entry,
|
|
102
|
+
})) {
|
|
103
|
+
succession = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (vetoes > 0) {
|
|
107
|
+
return { state: "vetoed", successionByRecordKey: false, cacheSeconds };
|
|
108
|
+
}
|
|
109
|
+
if (confirms === 0) {
|
|
110
|
+
return { state: "pending", successionByRecordKey: false, cacheSeconds };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
state: succession ? "confirmed-rotation" : "confirmed-recovery",
|
|
114
|
+
successionByRecordKey: succession,
|
|
115
|
+
cacheSeconds,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|