@zuoyehaoduoa/wire 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 +668 -0
- package/dist/index.cjs +880 -0
- package/dist/index.d.cts +2500 -0
- package/dist/index.d.mts +2500 -0
- package/dist/index.mjs +796 -0
- package/package.json +62 -0
- package/src/index.ts +5 -0
- package/src/messageMeta.ts +24 -0
- package/src/messages.test.ts +165 -0
- package/src/messages.ts +97 -0
- package/src/remote-auth-errors.ts +87 -0
- package/src/remote-crypto.test.ts +244 -0
- package/src/remote-crypto.ts +778 -0
- package/src/remote-protocol.test.ts +61 -0
- package/src/remote-protocol.ts +53 -0
- package/src/sessionProtocol.test.ts +170 -0
- package/src/sessionProtocol.ts +141 -0
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zuoyehaoduoa/wire",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared message wire types and Zod schemas for codex-deck clients and services",
|
|
5
|
+
"author": "Kirill Dubovitskiy",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"homepage": "https://github.com/asfsdsf/codex-deck/tree/main/wire",
|
|
9
|
+
"bugs": "https://github.com/asfsdsf/codex-deck/issues",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/asfsdsf/codex-deck.git",
|
|
13
|
+
"directory": "wire"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.cjs",
|
|
16
|
+
"module": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.cts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"require": {
|
|
21
|
+
"types": "./dist/index.d.cts",
|
|
22
|
+
"default": "./dist/index.cjs"
|
|
23
|
+
},
|
|
24
|
+
"import": {
|
|
25
|
+
"types": "./dist/index.d.mts",
|
|
26
|
+
"default": "./dist/index.mjs"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"dist",
|
|
33
|
+
"package.json",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"build": "shx rm -rf dist && pnpm exec tsc --noEmit && pnpm exec pkgroll",
|
|
39
|
+
"test": "$npm_execpath run build && vitest run",
|
|
40
|
+
"prepublishOnly": "$npm_execpath run build && $npm_execpath run test",
|
|
41
|
+
"release": "pnpm exec release-it"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@paralleldrive/cuid2": "^2.2.2",
|
|
45
|
+
"@serenity-kit/opaque": "1.1.0",
|
|
46
|
+
"tweetnacl": "^1.0.3",
|
|
47
|
+
"zod": "3.25.76"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": ">=20",
|
|
51
|
+
"pkgroll": "^2.14.2",
|
|
52
|
+
"release-it": "^19.0.6",
|
|
53
|
+
"shx": "^0.3.3",
|
|
54
|
+
"typescript": "5.9.3",
|
|
55
|
+
"vitest": "^3.2.4"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"registry": "https://registry.npmjs.org",
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"packageManager": "pnpm@10.32.1"
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
export const MessageMetaSchema = z.object({
|
|
4
|
+
sentFrom: z.string().optional(),
|
|
5
|
+
permissionMode: z
|
|
6
|
+
.enum([
|
|
7
|
+
"default",
|
|
8
|
+
"acceptEdits",
|
|
9
|
+
"bypassPermissions",
|
|
10
|
+
"plan",
|
|
11
|
+
"read-only",
|
|
12
|
+
"safe-yolo",
|
|
13
|
+
"yolo",
|
|
14
|
+
])
|
|
15
|
+
.optional(),
|
|
16
|
+
model: z.string().nullable().optional(),
|
|
17
|
+
fallbackModel: z.string().nullable().optional(),
|
|
18
|
+
customSystemPrompt: z.string().nullable().optional(),
|
|
19
|
+
appendSystemPrompt: z.string().nullable().optional(),
|
|
20
|
+
allowedTools: z.array(z.string()).nullable().optional(),
|
|
21
|
+
disallowedTools: z.array(z.string()).nullable().optional(),
|
|
22
|
+
displayText: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
export type MessageMeta = z.infer<typeof MessageMetaSchema>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
CoreUpdateContainerSchema,
|
|
4
|
+
MessageContentSchema,
|
|
5
|
+
SessionProtocolMessageSchema,
|
|
6
|
+
UpdateMachineBodySchema,
|
|
7
|
+
UpdateNewMessageBodySchema,
|
|
8
|
+
UpdateSessionBodySchema,
|
|
9
|
+
} from "./messages";
|
|
10
|
+
|
|
11
|
+
describe("shared wire message schemas", () => {
|
|
12
|
+
it("parses a new-message update", () => {
|
|
13
|
+
const parsed = UpdateNewMessageBodySchema.safeParse({
|
|
14
|
+
t: "new-message",
|
|
15
|
+
sid: "session-1",
|
|
16
|
+
message: {
|
|
17
|
+
id: "msg-1",
|
|
18
|
+
seq: 10,
|
|
19
|
+
localId: null,
|
|
20
|
+
content: {
|
|
21
|
+
t: "encrypted",
|
|
22
|
+
c: "ZmFrZS1lbmNyeXB0ZWQ=",
|
|
23
|
+
},
|
|
24
|
+
createdAt: 123,
|
|
25
|
+
updatedAt: 124,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(parsed.success).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("parses update-session with nullable agentState value", () => {
|
|
33
|
+
const parsed = UpdateSessionBodySchema.safeParse({
|
|
34
|
+
t: "update-session",
|
|
35
|
+
id: "session-1",
|
|
36
|
+
metadata: {
|
|
37
|
+
version: 2,
|
|
38
|
+
value: "abc",
|
|
39
|
+
},
|
|
40
|
+
agentState: {
|
|
41
|
+
version: 3,
|
|
42
|
+
value: null,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(parsed.success).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("parses update-machine with optional activity fields", () => {
|
|
50
|
+
const parsed = UpdateMachineBodySchema.safeParse({
|
|
51
|
+
t: "update-machine",
|
|
52
|
+
machineId: "machine-1",
|
|
53
|
+
metadata: {
|
|
54
|
+
version: 1,
|
|
55
|
+
value: "abc",
|
|
56
|
+
},
|
|
57
|
+
daemonState: {
|
|
58
|
+
version: 2,
|
|
59
|
+
value: "def",
|
|
60
|
+
},
|
|
61
|
+
active: true,
|
|
62
|
+
activeAt: 12345,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(parsed.success).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("parses container updates for all shared update variants", () => {
|
|
69
|
+
const examples = [
|
|
70
|
+
{
|
|
71
|
+
id: "upd-1",
|
|
72
|
+
seq: 1,
|
|
73
|
+
body: {
|
|
74
|
+
t: "new-message",
|
|
75
|
+
sid: "session-1",
|
|
76
|
+
message: {
|
|
77
|
+
id: "msg-1",
|
|
78
|
+
seq: 1,
|
|
79
|
+
localId: null,
|
|
80
|
+
content: { t: "encrypted", c: "x" },
|
|
81
|
+
createdAt: 1,
|
|
82
|
+
updatedAt: 1,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
createdAt: 1,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "upd-2",
|
|
89
|
+
seq: 2,
|
|
90
|
+
body: {
|
|
91
|
+
t: "update-session",
|
|
92
|
+
id: "session-1",
|
|
93
|
+
metadata: null,
|
|
94
|
+
agentState: {
|
|
95
|
+
version: 1,
|
|
96
|
+
value: null,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
createdAt: 2,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "upd-3",
|
|
103
|
+
seq: 3,
|
|
104
|
+
body: {
|
|
105
|
+
t: "update-machine",
|
|
106
|
+
machineId: "machine-1",
|
|
107
|
+
metadata: null,
|
|
108
|
+
daemonState: null,
|
|
109
|
+
},
|
|
110
|
+
createdAt: 3,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (const sample of examples) {
|
|
115
|
+
expect(CoreUpdateContainerSchema.safeParse(sample).success).toBe(true);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("parses modern session protocol wrapper payload", () => {
|
|
120
|
+
const parsed = SessionProtocolMessageSchema.safeParse({
|
|
121
|
+
role: "session",
|
|
122
|
+
content: {
|
|
123
|
+
id: "msg-1",
|
|
124
|
+
time: 1000,
|
|
125
|
+
role: "agent",
|
|
126
|
+
turn: "turn-1",
|
|
127
|
+
ev: {
|
|
128
|
+
t: "text",
|
|
129
|
+
text: "hello",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
meta: {
|
|
133
|
+
sentFrom: "cli",
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(parsed.success).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("parses top-level message content only for session protocol payloads", () => {
|
|
141
|
+
const modernParsed = MessageContentSchema.safeParse({
|
|
142
|
+
role: "session",
|
|
143
|
+
content: {
|
|
144
|
+
id: "msg-2",
|
|
145
|
+
time: 2000,
|
|
146
|
+
role: "agent",
|
|
147
|
+
turn: "turn-2",
|
|
148
|
+
ev: {
|
|
149
|
+
t: "text",
|
|
150
|
+
text: "hello from session protocol",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const legacyParsed = MessageContentSchema.safeParse({
|
|
155
|
+
role: "user",
|
|
156
|
+
content: {
|
|
157
|
+
type: "text",
|
|
158
|
+
text: "hello from user",
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(modernParsed.success).toBe(true);
|
|
163
|
+
expect(legacyParsed.success).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { sessionEnvelopeSchema } from "./sessionProtocol";
|
|
3
|
+
import { MessageMetaSchema, type MessageMeta } from "./messageMeta";
|
|
4
|
+
|
|
5
|
+
export const SessionMessageContentSchema = z.object({
|
|
6
|
+
c: z.string(),
|
|
7
|
+
t: z.literal("encrypted"),
|
|
8
|
+
});
|
|
9
|
+
export type SessionMessageContent = z.infer<typeof SessionMessageContentSchema>;
|
|
10
|
+
|
|
11
|
+
export const SessionMessageSchema = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
seq: z.number(),
|
|
14
|
+
localId: z.string().nullish(),
|
|
15
|
+
content: SessionMessageContentSchema,
|
|
16
|
+
createdAt: z.number(),
|
|
17
|
+
updatedAt: z.number(),
|
|
18
|
+
});
|
|
19
|
+
export type SessionMessage = z.infer<typeof SessionMessageSchema>;
|
|
20
|
+
export { MessageMetaSchema };
|
|
21
|
+
export type { MessageMeta };
|
|
22
|
+
|
|
23
|
+
export const SessionProtocolMessageSchema = z.object({
|
|
24
|
+
role: z.literal("session"),
|
|
25
|
+
content: sessionEnvelopeSchema,
|
|
26
|
+
meta: MessageMetaSchema.optional(),
|
|
27
|
+
});
|
|
28
|
+
export type SessionProtocolMessage = z.infer<
|
|
29
|
+
typeof SessionProtocolMessageSchema
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
export const MessageContentSchema = SessionProtocolMessageSchema;
|
|
33
|
+
export type MessageContent = z.infer<typeof MessageContentSchema>;
|
|
34
|
+
|
|
35
|
+
export const VersionedEncryptedValueSchema = z.object({
|
|
36
|
+
version: z.number(),
|
|
37
|
+
value: z.string(),
|
|
38
|
+
});
|
|
39
|
+
export type VersionedEncryptedValue = z.infer<
|
|
40
|
+
typeof VersionedEncryptedValueSchema
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
export const VersionedNullableEncryptedValueSchema = z.object({
|
|
44
|
+
version: z.number(),
|
|
45
|
+
value: z.string().nullable(),
|
|
46
|
+
});
|
|
47
|
+
export type VersionedNullableEncryptedValue = z.infer<
|
|
48
|
+
typeof VersionedNullableEncryptedValueSchema
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
export const UpdateNewMessageBodySchema = z.object({
|
|
52
|
+
t: z.literal("new-message"),
|
|
53
|
+
sid: z.string(),
|
|
54
|
+
message: SessionMessageSchema,
|
|
55
|
+
});
|
|
56
|
+
export type UpdateNewMessageBody = z.infer<typeof UpdateNewMessageBodySchema>;
|
|
57
|
+
|
|
58
|
+
export const UpdateSessionBodySchema = z.object({
|
|
59
|
+
t: z.literal("update-session"),
|
|
60
|
+
id: z.string(),
|
|
61
|
+
metadata: VersionedEncryptedValueSchema.nullish(),
|
|
62
|
+
agentState: VersionedNullableEncryptedValueSchema.nullish(),
|
|
63
|
+
});
|
|
64
|
+
export type UpdateSessionBody = z.infer<typeof UpdateSessionBodySchema>;
|
|
65
|
+
|
|
66
|
+
export const VersionedMachineEncryptedValueSchema = z.object({
|
|
67
|
+
version: z.number(),
|
|
68
|
+
value: z.string(),
|
|
69
|
+
});
|
|
70
|
+
export type VersionedMachineEncryptedValue = z.infer<
|
|
71
|
+
typeof VersionedMachineEncryptedValueSchema
|
|
72
|
+
>;
|
|
73
|
+
|
|
74
|
+
export const UpdateMachineBodySchema = z.object({
|
|
75
|
+
t: z.literal("update-machine"),
|
|
76
|
+
machineId: z.string(),
|
|
77
|
+
metadata: VersionedMachineEncryptedValueSchema.nullish(),
|
|
78
|
+
daemonState: VersionedMachineEncryptedValueSchema.nullish(),
|
|
79
|
+
active: z.boolean().optional(),
|
|
80
|
+
activeAt: z.number().optional(),
|
|
81
|
+
});
|
|
82
|
+
export type UpdateMachineBody = z.infer<typeof UpdateMachineBodySchema>;
|
|
83
|
+
|
|
84
|
+
export const CoreUpdateBodySchema = z.discriminatedUnion("t", [
|
|
85
|
+
UpdateNewMessageBodySchema,
|
|
86
|
+
UpdateSessionBodySchema,
|
|
87
|
+
UpdateMachineBodySchema,
|
|
88
|
+
]);
|
|
89
|
+
export type CoreUpdateBody = z.infer<typeof CoreUpdateBodySchema>;
|
|
90
|
+
|
|
91
|
+
export const CoreUpdateContainerSchema = z.object({
|
|
92
|
+
id: z.string(),
|
|
93
|
+
seq: z.number(),
|
|
94
|
+
body: CoreUpdateBodySchema,
|
|
95
|
+
createdAt: z.number(),
|
|
96
|
+
});
|
|
97
|
+
export type CoreUpdateContainer = z.infer<typeof CoreUpdateContainerSchema>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const RemoteAuthErrorCode = {
|
|
2
|
+
accountNotFound: "remote_account_not_found",
|
|
3
|
+
accountMismatch: "remote_account_mismatch",
|
|
4
|
+
setupTokensMissing: "remote_setup_tokens_missing",
|
|
5
|
+
setupTokenInvalid: "remote_setup_token_invalid",
|
|
6
|
+
loginAlreadyBound: "remote_login_already_bound",
|
|
7
|
+
machineAlreadyBound: "remote_machine_already_bound",
|
|
8
|
+
invalidLogin: "remote_invalid_login",
|
|
9
|
+
invalidRegister: "remote_invalid_register",
|
|
10
|
+
invalidLoginState: "remote_invalid_login_state",
|
|
11
|
+
adminPasswordInvalid: "remote_admin_password_invalid",
|
|
12
|
+
authUpgradeRequired: "remote_auth_upgrade_required",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type RemoteAuthErrorCode =
|
|
16
|
+
(typeof RemoteAuthErrorCode)[keyof typeof RemoteAuthErrorCode];
|
|
17
|
+
|
|
18
|
+
export type RemoteAuthHintContext = "cli" | "browser" | "admin";
|
|
19
|
+
|
|
20
|
+
const REMOTE_AUTH_ERROR_CODE_SET = new Set<string>(
|
|
21
|
+
Object.values(RemoteAuthErrorCode),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export function isRemoteAuthErrorCode(
|
|
25
|
+
value: unknown,
|
|
26
|
+
): value is RemoteAuthErrorCode {
|
|
27
|
+
return typeof value === "string" && REMOTE_AUTH_ERROR_CODE_SET.has(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getRemoteAuthHint(params: {
|
|
31
|
+
code?: string | null;
|
|
32
|
+
error?: string | null;
|
|
33
|
+
context: RemoteAuthHintContext;
|
|
34
|
+
}): string {
|
|
35
|
+
const code = isRemoteAuthErrorCode(params.code) ? params.code : null;
|
|
36
|
+
if (code === RemoteAuthErrorCode.invalidLogin) {
|
|
37
|
+
if (params.context === "cli") {
|
|
38
|
+
return "Remote password is incorrect for this registered CLI account. Update --remote-password (or CODEXDECK_REMOTE_PASSWORD) to match the target server credentials.";
|
|
39
|
+
}
|
|
40
|
+
if (params.context === "admin") {
|
|
41
|
+
return "Admin password is incorrect.";
|
|
42
|
+
}
|
|
43
|
+
return "Incorrect password for this remote username. Use the same password configured on the target CLI.";
|
|
44
|
+
}
|
|
45
|
+
if (code === RemoteAuthErrorCode.accountNotFound) {
|
|
46
|
+
if (params.context === "cli") {
|
|
47
|
+
return "Remote account not found. Register this CLI first using a valid --remote-setup-token, --remote-username, and --remote-password.";
|
|
48
|
+
}
|
|
49
|
+
return "Remote account not found. Start the CLI first with --remote-setup-token, --remote-username, and --remote-password.";
|
|
50
|
+
}
|
|
51
|
+
if (code === RemoteAuthErrorCode.accountMismatch) {
|
|
52
|
+
return "This login is bound to a different machine. Use the credentials originally registered for this machine, or configure a different --remote-machine-id.";
|
|
53
|
+
}
|
|
54
|
+
if (code === RemoteAuthErrorCode.setupTokenInvalid) {
|
|
55
|
+
return "Setup token is invalid or disabled. Generate a valid setup token in the server admin page and retry.";
|
|
56
|
+
}
|
|
57
|
+
if (code === RemoteAuthErrorCode.setupTokensMissing) {
|
|
58
|
+
return "Remote server has no setup tokens configured. Add setup tokens in server admin before registering a CLI.";
|
|
59
|
+
}
|
|
60
|
+
if (code === RemoteAuthErrorCode.invalidLoginState) {
|
|
61
|
+
return "Remote login attempt expired or is no longer valid. Retry login.";
|
|
62
|
+
}
|
|
63
|
+
if (code === RemoteAuthErrorCode.invalidRegister) {
|
|
64
|
+
return "Remote registration attempt expired or is no longer valid. Retry CLI registration.";
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
code === RemoteAuthErrorCode.loginAlreadyBound ||
|
|
68
|
+
code === RemoteAuthErrorCode.machineAlreadyBound
|
|
69
|
+
) {
|
|
70
|
+
return "This remote login or machine is already bound to different credentials. Verify --remote-username, --remote-password, and --remote-machine-id.";
|
|
71
|
+
}
|
|
72
|
+
if (code === RemoteAuthErrorCode.authUpgradeRequired) {
|
|
73
|
+
return "Legacy remote auth is no longer supported. Re-register this CLI using the current codex-deck version.";
|
|
74
|
+
}
|
|
75
|
+
if (code === RemoteAuthErrorCode.adminPasswordInvalid) {
|
|
76
|
+
return "Admin password is incorrect.";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fallback = params.error?.trim();
|
|
80
|
+
if (fallback) {
|
|
81
|
+
return fallback;
|
|
82
|
+
}
|
|
83
|
+
if (params.context === "admin") {
|
|
84
|
+
return "Remote admin login failed.";
|
|
85
|
+
}
|
|
86
|
+
return "Remote login failed.";
|
|
87
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createRemoteOpaqueRegistrationResponse,
|
|
4
|
+
createRemoteOpaqueServerSetup,
|
|
5
|
+
decodeBase64,
|
|
6
|
+
decryptRemotePayload,
|
|
7
|
+
deriveRemoteLoginHandle,
|
|
8
|
+
deriveRemoteRelayKeyFromExportKey,
|
|
9
|
+
encryptRemotePayload,
|
|
10
|
+
finishRemoteOpaqueLogin,
|
|
11
|
+
finishRemoteOpaqueRegistration,
|
|
12
|
+
finishRemoteOpaqueServerLogin,
|
|
13
|
+
generateRemoteRpcSigningKeyPair,
|
|
14
|
+
getRemoteOpaqueServerPublicKey,
|
|
15
|
+
signRemoteRpcResult,
|
|
16
|
+
startRemoteOpaqueLogin,
|
|
17
|
+
startRemoteOpaqueRegistration,
|
|
18
|
+
startRemoteOpaqueServerLogin,
|
|
19
|
+
verifyRemoteRpcResultSignature,
|
|
20
|
+
} from "./remote-crypto";
|
|
21
|
+
|
|
22
|
+
describe("remote-crypto", () => {
|
|
23
|
+
const username = "alice";
|
|
24
|
+
const password = "correct horse battery staple";
|
|
25
|
+
const realmId = "realm-a";
|
|
26
|
+
|
|
27
|
+
it("derives a stable opaque login handle", async () => {
|
|
28
|
+
const first = await deriveRemoteLoginHandle(username, realmId);
|
|
29
|
+
const second = await deriveRemoteLoginHandle(username, realmId);
|
|
30
|
+
expect(first).toBe(second);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("completes opaque registration and login with a stable export key", async () => {
|
|
34
|
+
const serverSetup = await createRemoteOpaqueServerSetup();
|
|
35
|
+
const serverPublicKey = await getRemoteOpaqueServerPublicKey(serverSetup);
|
|
36
|
+
const loginHandle = await deriveRemoteLoginHandle(username, realmId);
|
|
37
|
+
|
|
38
|
+
const registrationStart = await startRemoteOpaqueRegistration(password);
|
|
39
|
+
const { registrationResponse } =
|
|
40
|
+
await createRemoteOpaqueRegistrationResponse({
|
|
41
|
+
serverSetup,
|
|
42
|
+
loginHandle,
|
|
43
|
+
registrationRequest: registrationStart.registrationRequest,
|
|
44
|
+
});
|
|
45
|
+
const registrationFinish = await finishRemoteOpaqueRegistration({
|
|
46
|
+
password,
|
|
47
|
+
clientRegistrationState: registrationStart.clientRegistrationState,
|
|
48
|
+
registrationResponse,
|
|
49
|
+
loginHandle,
|
|
50
|
+
realmId,
|
|
51
|
+
expectedServerPublicKey: serverPublicKey,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const loginStart = await startRemoteOpaqueLogin(password);
|
|
55
|
+
const loginResponse = await startRemoteOpaqueServerLogin({
|
|
56
|
+
serverSetup,
|
|
57
|
+
registrationRecord: registrationFinish.registrationRecord,
|
|
58
|
+
startLoginRequest: loginStart.startLoginRequest,
|
|
59
|
+
loginHandle,
|
|
60
|
+
realmId,
|
|
61
|
+
});
|
|
62
|
+
const loginFinish = await finishRemoteOpaqueLogin({
|
|
63
|
+
password,
|
|
64
|
+
clientLoginState: loginStart.clientLoginState,
|
|
65
|
+
loginResponse: loginResponse.loginResponse,
|
|
66
|
+
loginHandle,
|
|
67
|
+
realmId,
|
|
68
|
+
expectedServerPublicKey: serverPublicKey,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(loginFinish).not.toBeNull();
|
|
72
|
+
expect(loginFinish?.exportKey).toBe(registrationFinish.exportKey);
|
|
73
|
+
expect(Array.from(loginFinish?.relayKey || [])).toEqual(
|
|
74
|
+
Array.from(registrationFinish.relayKey),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const serverFinish = await finishRemoteOpaqueServerLogin({
|
|
78
|
+
serverLoginState: loginResponse.serverLoginState,
|
|
79
|
+
finishLoginRequest: loginFinish!.finishLoginRequest,
|
|
80
|
+
loginHandle,
|
|
81
|
+
realmId,
|
|
82
|
+
});
|
|
83
|
+
expect(serverFinish.sessionKey).toMatch(/\S+/);
|
|
84
|
+
expect(loginFinish?.sessionKey).toMatch(/\S+/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("rejects opaque login with the wrong password", async () => {
|
|
88
|
+
const serverSetup = await createRemoteOpaqueServerSetup();
|
|
89
|
+
const loginHandle = await deriveRemoteLoginHandle(username, realmId);
|
|
90
|
+
|
|
91
|
+
const registrationStart = await startRemoteOpaqueRegistration(password);
|
|
92
|
+
const { registrationResponse } =
|
|
93
|
+
await createRemoteOpaqueRegistrationResponse({
|
|
94
|
+
serverSetup,
|
|
95
|
+
loginHandle,
|
|
96
|
+
registrationRequest: registrationStart.registrationRequest,
|
|
97
|
+
});
|
|
98
|
+
const registrationFinish = await finishRemoteOpaqueRegistration({
|
|
99
|
+
password,
|
|
100
|
+
clientRegistrationState: registrationStart.clientRegistrationState,
|
|
101
|
+
registrationResponse,
|
|
102
|
+
loginHandle,
|
|
103
|
+
realmId,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const loginStart = await startRemoteOpaqueLogin("wrong password");
|
|
107
|
+
const loginResponse = await startRemoteOpaqueServerLogin({
|
|
108
|
+
serverSetup,
|
|
109
|
+
registrationRecord: registrationFinish.registrationRecord,
|
|
110
|
+
startLoginRequest: loginStart.startLoginRequest,
|
|
111
|
+
loginHandle,
|
|
112
|
+
realmId,
|
|
113
|
+
});
|
|
114
|
+
const loginFinish = await finishRemoteOpaqueLogin({
|
|
115
|
+
password: "wrong password",
|
|
116
|
+
clientLoginState: loginStart.clientLoginState,
|
|
117
|
+
loginResponse: loginResponse.loginResponse,
|
|
118
|
+
loginHandle,
|
|
119
|
+
realmId,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(loginFinish).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("derives a stable relay key from an export key", async () => {
|
|
126
|
+
const serverSetup = await createRemoteOpaqueServerSetup();
|
|
127
|
+
const loginHandle = await deriveRemoteLoginHandle(username, realmId);
|
|
128
|
+
|
|
129
|
+
const registrationStart = await startRemoteOpaqueRegistration(password);
|
|
130
|
+
const { registrationResponse } =
|
|
131
|
+
await createRemoteOpaqueRegistrationResponse({
|
|
132
|
+
serverSetup,
|
|
133
|
+
loginHandle,
|
|
134
|
+
registrationRequest: registrationStart.registrationRequest,
|
|
135
|
+
});
|
|
136
|
+
const registrationFinish = await finishRemoteOpaqueRegistration({
|
|
137
|
+
password,
|
|
138
|
+
clientRegistrationState: registrationStart.clientRegistrationState,
|
|
139
|
+
registrationResponse,
|
|
140
|
+
loginHandle,
|
|
141
|
+
realmId,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const derived = await deriveRemoteRelayKeyFromExportKey(
|
|
145
|
+
registrationFinish.exportKey,
|
|
146
|
+
);
|
|
147
|
+
expect(Array.from(derived)).toEqual(
|
|
148
|
+
Array.from(registrationFinish.relayKey),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("decodes opaque base64url export keys", async () => {
|
|
153
|
+
const serverSetup = await createRemoteOpaqueServerSetup();
|
|
154
|
+
const loginHandle = await deriveRemoteLoginHandle(username, realmId);
|
|
155
|
+
|
|
156
|
+
const registrationStart = await startRemoteOpaqueRegistration(password);
|
|
157
|
+
const { registrationResponse } =
|
|
158
|
+
await createRemoteOpaqueRegistrationResponse({
|
|
159
|
+
serverSetup,
|
|
160
|
+
loginHandle,
|
|
161
|
+
registrationRequest: registrationStart.registrationRequest,
|
|
162
|
+
});
|
|
163
|
+
const registrationFinish = await finishRemoteOpaqueRegistration({
|
|
164
|
+
password,
|
|
165
|
+
clientRegistrationState: registrationStart.clientRegistrationState,
|
|
166
|
+
registrationResponse,
|
|
167
|
+
loginHandle,
|
|
168
|
+
realmId,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(registrationFinish.exportKey).toMatch(/[-_]/);
|
|
172
|
+
expect(decodeBase64(registrationFinish.exportKey)).toHaveLength(64);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("encrypts and decrypts relay payloads with an opaque-derived relay key", async () => {
|
|
176
|
+
const serverSetup = await createRemoteOpaqueServerSetup();
|
|
177
|
+
const loginHandle = await deriveRemoteLoginHandle(username, realmId);
|
|
178
|
+
|
|
179
|
+
const registrationStart = await startRemoteOpaqueRegistration(password);
|
|
180
|
+
const { registrationResponse } =
|
|
181
|
+
await createRemoteOpaqueRegistrationResponse({
|
|
182
|
+
serverSetup,
|
|
183
|
+
loginHandle,
|
|
184
|
+
registrationRequest: registrationStart.registrationRequest,
|
|
185
|
+
});
|
|
186
|
+
const registrationFinish = await finishRemoteOpaqueRegistration({
|
|
187
|
+
password,
|
|
188
|
+
clientRegistrationState: registrationStart.clientRegistrationState,
|
|
189
|
+
registrationResponse,
|
|
190
|
+
loginHandle,
|
|
191
|
+
realmId,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const encrypted = await encryptRemotePayload(registrationFinish.relayKey, {
|
|
195
|
+
requestId: "req-1",
|
|
196
|
+
body: {
|
|
197
|
+
path: "/api/sessions",
|
|
198
|
+
method: "GET",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const decrypted = await decryptRemotePayload<{
|
|
203
|
+
requestId: string;
|
|
204
|
+
body: { path: string; method: string };
|
|
205
|
+
}>(registrationFinish.relayKey, encrypted);
|
|
206
|
+
|
|
207
|
+
expect(decrypted).toEqual({
|
|
208
|
+
requestId: "req-1",
|
|
209
|
+
body: {
|
|
210
|
+
path: "/api/sessions",
|
|
211
|
+
method: "GET",
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("signs and verifies remote RPC result payloads", () => {
|
|
217
|
+
const keyPair = generateRemoteRpcSigningKeyPair();
|
|
218
|
+
const signature = signRemoteRpcResult({
|
|
219
|
+
machineId: "machine-a",
|
|
220
|
+
requestId: "req-1",
|
|
221
|
+
encryptedResult: "ciphertext",
|
|
222
|
+
secretKey: keyPair.secretKey,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(
|
|
226
|
+
verifyRemoteRpcResultSignature({
|
|
227
|
+
machineId: "machine-a",
|
|
228
|
+
requestId: "req-1",
|
|
229
|
+
encryptedResult: "ciphertext",
|
|
230
|
+
signature,
|
|
231
|
+
publicKey: keyPair.publicKey,
|
|
232
|
+
}),
|
|
233
|
+
).toBe(true);
|
|
234
|
+
expect(
|
|
235
|
+
verifyRemoteRpcResultSignature({
|
|
236
|
+
machineId: "machine-a",
|
|
237
|
+
requestId: "req-1",
|
|
238
|
+
encryptedResult: "tampered",
|
|
239
|
+
signature,
|
|
240
|
+
publicKey: keyPair.publicKey,
|
|
241
|
+
}),
|
|
242
|
+
).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|