@atomicmail/agent-skill 0.1.0 → 0.2.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.
Files changed (64) hide show
  1. package/README.md +91 -0
  2. package/SKILL.md +53 -193
  3. package/esm/_dnt.polyfills.d.ts +101 -0
  4. package/esm/_dnt.polyfills.d.ts.map +1 -0
  5. package/esm/_dnt.polyfills.js +127 -0
  6. package/esm/{skill/scripts/lib/auth.d.ts → lib/agent/auth/agent-auth-http.d.ts} +1 -17
  7. package/esm/lib/agent/auth/agent-auth-http.d.ts.map +1 -0
  8. package/esm/lib/agent/auth/agent-auth-http.js +76 -0
  9. package/esm/lib/agent/auth/agent-jwt.d.ts +14 -0
  10. package/esm/lib/agent/auth/agent-jwt.d.ts.map +1 -0
  11. package/esm/lib/agent/auth/agent-jwt.js +29 -0
  12. package/esm/lib/agent/auth/agent-pow.d.ts +5 -0
  13. package/esm/lib/agent/auth/agent-pow.d.ts.map +1 -0
  14. package/esm/lib/agent/auth/agent-pow.js +49 -0
  15. package/esm/lib/agent/jmap/agent-help-content.d.ts +4 -0
  16. package/esm/lib/agent/jmap/agent-help-content.d.ts.map +1 -0
  17. package/esm/lib/agent/jmap/agent-help-content.js +244 -0
  18. package/esm/lib/agent/jmap/agent-jmap.d.ts +49 -0
  19. package/esm/lib/agent/jmap/agent-jmap.d.ts.map +1 -0
  20. package/esm/lib/agent/jmap/agent-jmap.js +174 -0
  21. package/esm/lib/agent/jmap/agent-vars.d.ts +23 -0
  22. package/esm/lib/agent/jmap/agent-vars.d.ts.map +1 -0
  23. package/esm/lib/agent/jmap/agent-vars.js +65 -0
  24. package/esm/{skill/scripts/lib/credentials.d.ts → lib/agent/session/agent-credentials-store.d.ts} +4 -1
  25. package/esm/lib/agent/session/agent-credentials-store.d.ts.map +1 -0
  26. package/esm/{skill/scripts/lib/credentials.js → lib/agent/session/agent-credentials-store.js} +28 -8
  27. package/esm/lib/agent/session/agent-resolve-config.d.ts +24 -0
  28. package/esm/lib/agent/session/agent-resolve-config.d.ts.map +1 -0
  29. package/esm/lib/agent/session/agent-resolve-config.js +70 -0
  30. package/esm/lib/agent/session/agent-session.d.ts +62 -0
  31. package/esm/lib/agent/session/agent-session.d.ts.map +1 -0
  32. package/esm/lib/agent/session/agent-session.js +206 -0
  33. package/esm/lib/core/consts.d.ts.map +1 -0
  34. package/esm/lib/core/types.d.ts +2 -0
  35. package/esm/lib/core/types.d.ts.map +1 -0
  36. package/esm/lib/core/types.js +1 -0
  37. package/esm/lib/core/utils.d.ts +10 -0
  38. package/esm/lib/core/utils.d.ts.map +1 -0
  39. package/esm/lib/core/utils.js +28 -0
  40. package/esm/lib/mod.d.ts +14 -0
  41. package/esm/lib/mod.d.ts.map +1 -0
  42. package/esm/lib/mod.js +13 -0
  43. package/esm/lib/network/auth-client.d.ts +57 -0
  44. package/esm/lib/network/auth-client.d.ts.map +1 -0
  45. package/esm/lib/network/auth-client.js +188 -0
  46. package/esm/skill/cli.d.ts +3 -0
  47. package/esm/skill/cli.d.ts.map +1 -0
  48. package/esm/skill/cli.js +306 -0
  49. package/package.json +5 -6
  50. package/presets/list_inbox.json +39 -0
  51. package/presets/reply.json +75 -0
  52. package/presets/send_mail.json +42 -0
  53. package/esm/lib/src/consts.d.ts.map +0 -1
  54. package/esm/skill/scripts/jmap_request.d.ts +0 -3
  55. package/esm/skill/scripts/jmap_request.d.ts.map +0 -1
  56. package/esm/skill/scripts/jmap_request.js +0 -265
  57. package/esm/skill/scripts/lib/auth.d.ts.map +0 -1
  58. package/esm/skill/scripts/lib/auth.js +0 -163
  59. package/esm/skill/scripts/lib/credentials.d.ts.map +0 -1
  60. package/esm/skill/scripts/signup.d.ts +0 -3
  61. package/esm/skill/scripts/signup.d.ts.map +0 -1
  62. package/esm/skill/scripts/signup.js +0 -170
  63. /package/esm/lib/{src → core}/consts.d.ts +0 -0
  64. /package/esm/lib/{src → core}/consts.js +0 -0
@@ -0,0 +1,75 @@
1
+ {
2
+ "using": [
3
+ "urn:ietf:params:jmap:core",
4
+ "urn:ietf:params:jmap:mail",
5
+ "urn:ietf:params:jmap:submission"
6
+ ],
7
+ "methodCalls": [
8
+ [
9
+ "Email/get",
10
+ {
11
+ "accountId": "$ACCOUNT_ID",
12
+ "ids": ["$MAIL_ID"],
13
+ "properties": [
14
+ "id",
15
+ "threadId",
16
+ "from",
17
+ "replyTo",
18
+ "subject",
19
+ "messageId"
20
+ ]
21
+ },
22
+ "g0"
23
+ ],
24
+ [
25
+ "Email/set",
26
+ {
27
+ "accountId": "$ACCOUNT_ID",
28
+ "create": {
29
+ "d1": {
30
+ "from": [{ "email": "$INBOX" }],
31
+ "#to": {
32
+ "resultOf": "g0",
33
+ "name": "Email/get",
34
+ "path": "/list/0/replyTo"
35
+ },
36
+ "#subject": {
37
+ "resultOf": "g0",
38
+ "name": "Email/get",
39
+ "path": "/list/0/subject"
40
+ },
41
+ "#inReplyTo": {
42
+ "resultOf": "g0",
43
+ "name": "Email/get",
44
+ "path": "/list/0/messageId"
45
+ },
46
+ "textBody": [{ "partId": "b", "type": "text/plain" }],
47
+ "bodyValues": { "b": { "value": "$BODY" } },
48
+ "keywords": { "$draft": true }
49
+ }
50
+ }
51
+ },
52
+ "c0"
53
+ ],
54
+ [
55
+ "EmailSubmission/set",
56
+ {
57
+ "accountId": "$ACCOUNT_ID",
58
+ "create": {
59
+ "s1": {
60
+ "emailId": "#d1",
61
+ "envelope": {
62
+ "mailFrom": { "email": "$INBOX" },
63
+ "#rcptTo": {
64
+ "resultOf": "g0",
65
+ "name": "Email/get",
66
+ "path": "/list/0/replyTo"
67
+ }
68
+ }
69
+ }
70
+ }
71
+ },
72
+ "c1"
73
+ ]
74
+ ]
75
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "using": [
3
+ "urn:ietf:params:jmap:core",
4
+ "urn:ietf:params:jmap:mail",
5
+ "urn:ietf:params:jmap:submission"
6
+ ],
7
+ "methodCalls": [
8
+ [
9
+ "Email/set",
10
+ {
11
+ "accountId": "$ACCOUNT_ID",
12
+ "create": {
13
+ "d1": {
14
+ "from": [{ "email": "$INBOX" }],
15
+ "to": [{ "email": "$TO" }],
16
+ "subject": "$SUBJECT",
17
+ "textBody": [{ "partId": "b", "type": "text/plain" }],
18
+ "bodyValues": { "b": { "value": "$BODY" } },
19
+ "keywords": { "$draft": true }
20
+ }
21
+ }
22
+ },
23
+ "c0"
24
+ ],
25
+ [
26
+ "EmailSubmission/set",
27
+ {
28
+ "accountId": "$ACCOUNT_ID",
29
+ "create": {
30
+ "s1": {
31
+ "emailId": "#d1",
32
+ "envelope": {
33
+ "mailFrom": { "email": "$INBOX" },
34
+ "rcptTo": [{ "email": "$TO" }]
35
+ }
36
+ }
37
+ }
38
+ },
39
+ "c1"
40
+ ]
41
+ ]
42
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../../../src/lib/src/consts.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,qEAC4B,CAAC;AAErE,eAAO,MAAM,UAAU,OAAO,CAAC;AAC/B,eAAO,MAAM,UAAU,QAAkB,CAAC;AAC1C,eAAO,MAAM,WAAW,QAAkB,CAAC;AAC3C,eAAO,MAAM,UAAU,QAAmB,CAAC;AAC3C,eAAO,MAAM,YAAY,QAAkB,CAAC;AAC5C,eAAO,MAAM,WAAW,QAAmB,CAAC"}
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
3
- //# sourceMappingURL=jmap_request.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"jmap_request.d.ts","sourceRoot":"","sources":["../../../src/skill/scripts/jmap_request.ts"],"names":[],"mappings":""}
@@ -1,265 +0,0 @@
1
- #!/usr/bin/env node
2
- // Atomic Mail skill: JMAP request.
3
- //
4
- // Sends a JMAP request to api-service using credentials previously written
5
- // by signup. Auto-rotates session JWT (re-running PoW) and capability JWT
6
- // when they are within their safety margin of expiry, and writes the rotated
7
- // tokens back to disk so subsequent invocations stay fresh.
8
- //
9
- // JMAP method calls can be supplied two ways:
10
- // --ops '<json>' inline JSON body
11
- // --ops-file <path> read JSON body from a file (e.g. saved
12
- // "fetch_last_100.json" preset)
13
- //
14
- // In both cases, the JSON may be either:
15
- // • an array of methodCalls, e.g. [["Mailbox/get", {...}, "m0"]]
16
- // • a full envelope { using: [...], methodCalls: [...] }
17
- import process from "node:process";
18
- import { parseArgs } from "node:util";
19
- import { readFile } from "node:fs/promises";
20
- import { CAPABILITY_SAFETY_MARGIN_MS, fetchCapability, isJwtExpired, performPoWAndSession, SESSION_SAFETY_MARGIN_MS, } from "./lib/auth.js";
21
- import { defaultFilesFromOutDir, readCredentials, tryReadJwtFile, writeJwtFile, } from "./lib/credentials.js";
22
- const DEFAULT_USING = [
23
- "urn:ietf:params:jmap:core",
24
- "urn:ietf:params:jmap:mail",
25
- ];
26
- const HELP = `Usage: atomic-mail-jmap [OPTIONS]
27
-
28
- Send a JMAP request to your Atomic Mail inbox using credentials previously
29
- written by atomic-mail-signup. Capability and session JWTs are renewed
30
- automatically when expired.
31
-
32
- Token sources (defaults are based on --credentials-dir):
33
- --credentials-dir DIR Directory containing credentials.json,
34
- session.jwt, capability.jwt. Default: cwd.
35
- --credentials-file PATH Override path to credentials.json.
36
- --session-file PATH Override path to session.jwt.
37
- --capability-file PATH Override path to capability.jwt.
38
-
39
- Request body (one of these is required):
40
- --ops JSON Inline JSON: either an array of methodCalls or
41
- a full { using, methodCalls } object.
42
- --ops-file PATH Read --ops content from a file. Useful for
43
- saving reusable presets like 'fetch_last_100.json'.
44
- --session Fetch JMAP session metadata via
45
- GET /.well-known/jmap (no body).
46
-
47
- Other:
48
- --using LIST Comma-separated capability URNs. Overrides
49
- defaults: ${DEFAULT_USING.join(",")}.
50
- Ignored if the JSON envelope already sets 'using'.
51
- --dry-run Print the resolved request to stdout without
52
- sending it. Tokens are NOT rotated.
53
- --help, -h Show this help.
54
-
55
- Exit codes:
56
- 0 success — server response printed to stdout
57
- 1 network error or HTTP non-2xx response from api-service / auth-service
58
- 2 invalid CLI usage / malformed credentials or ops JSON
59
-
60
- Examples:
61
- # Inline ops
62
- atomic-mail-jmap --ops '[["Mailbox/get", {"accountId":"abc"}, "m0"]]'
63
-
64
- # Reusable preset file
65
- echo '[["Email/query",{"accountId":"abc","limit":100,"sort":[{"property":"receivedAt","isAscending":false}]},"q0"]]' \\
66
- > fetch_last_100.json
67
- atomic-mail-jmap --ops-file fetch_last_100.json
68
-
69
- # Session discovery
70
- atomic-mail-jmap --session
71
- `;
72
- function fail(message, code = 1) {
73
- process.stderr.write(`Error: ${message}\n`);
74
- if (code === 2)
75
- process.stderr.write("\nRun with --help for usage.\n");
76
- process.exit(code);
77
- }
78
- function readArgs() {
79
- let parsed;
80
- try {
81
- parsed = parseArgs({
82
- args: process.argv.slice(2),
83
- options: {
84
- "credentials-dir": { type: "string" },
85
- "credentials-file": { type: "string" },
86
- "session-file": { type: "string" },
87
- "capability-file": { type: "string" },
88
- ops: { type: "string" },
89
- "ops-file": { type: "string" },
90
- using: { type: "string" },
91
- session: { type: "boolean" },
92
- "dry-run": { type: "boolean" },
93
- help: { type: "boolean", short: "h" },
94
- },
95
- strict: true,
96
- allowPositionals: false,
97
- });
98
- }
99
- catch (err) {
100
- fail(err.message, 2);
101
- }
102
- if (parsed.values.help) {
103
- process.stdout.write(HELP);
104
- process.exit(0);
105
- }
106
- const dir = parsed.values["credentials-dir"] ?? ".";
107
- const defaults = defaultFilesFromOutDir(dir);
108
- const credentialsFile = parsed.values["credentials-file"] ??
109
- defaults.credentialsFile;
110
- const sessionFile = parsed.values["session-file"] ??
111
- defaults.sessionFile;
112
- const capabilityFile = parsed.values["capability-file"] ??
113
- defaults.capabilityFile;
114
- const ops = parsed.values.ops;
115
- const opsFile = parsed.values["ops-file"];
116
- const sessionMode = parsed.values.session === true;
117
- if (sessionMode && (ops || opsFile)) {
118
- fail("--session cannot be combined with --ops or --ops-file.", 2);
119
- }
120
- if (!sessionMode && !ops && !opsFile) {
121
- fail("Provide one of --ops, --ops-file, or --session.", 2);
122
- }
123
- if (ops && opsFile) {
124
- fail("--ops and --ops-file are mutually exclusive.", 2);
125
- }
126
- const usingFlag = parsed.values.using;
127
- const using = usingFlag
128
- ? usingFlag.split(",").map((s) => s.trim()).filter((s) => s.length > 0)
129
- : undefined;
130
- return {
131
- credentialsFile,
132
- sessionFile,
133
- capabilityFile,
134
- ops,
135
- opsFile,
136
- using,
137
- sessionMode,
138
- dryRun: parsed.values["dry-run"] === true,
139
- };
140
- }
141
- async function loadOps(args) {
142
- if (args.sessionMode)
143
- return undefined;
144
- let raw;
145
- if (args.ops !== undefined) {
146
- raw = args.ops;
147
- }
148
- else if (args.opsFile) {
149
- try {
150
- raw = await readFile(args.opsFile, "utf-8");
151
- }
152
- catch (err) {
153
- fail(`Could not read --ops-file '${args.opsFile}': ${err.message}`, 2);
154
- }
155
- }
156
- else {
157
- fail("Internal: ops not provided", 2);
158
- }
159
- let value;
160
- try {
161
- value = JSON.parse(raw);
162
- }
163
- catch (err) {
164
- fail(`Invalid JSON in JMAP ops: ${err.message}`, 2);
165
- }
166
- let using = args.using ?? DEFAULT_USING;
167
- let methodCalls;
168
- if (Array.isArray(value)) {
169
- methodCalls = value;
170
- }
171
- else if (value !== null &&
172
- typeof value === "object" &&
173
- Array.isArray(value.methodCalls)) {
174
- const obj = value;
175
- methodCalls = obj.methodCalls;
176
- // Inline `using` only takes precedence if user did not pass --using.
177
- if (Array.isArray(obj.using) && !args.using) {
178
- using = obj.using.filter((u) => typeof u === "string");
179
- }
180
- }
181
- else {
182
- fail('JMAP ops must be a methodCalls array, e.g. [["Mailbox/get",{...},"m0"]], ' +
183
- "or an object with a 'methodCalls' array.", 2);
184
- }
185
- return { using, methodCalls };
186
- }
187
- async function ensureFreshTokens(args) {
188
- const credentials = await readCredentials(args.credentialsFile);
189
- let sessionJWT = await tryReadJwtFile(args.sessionFile);
190
- let capabilityJWT = await tryReadJwtFile(args.capabilityFile);
191
- const sessionExpired = !sessionJWT ||
192
- isJwtExpired(sessionJWT, SESSION_SAFETY_MARGIN_MS);
193
- if (sessionExpired) {
194
- process.stderr.write("Session JWT missing or near expiry — re-running PoW.\n");
195
- const result = await performPoWAndSession({
196
- authUrl: credentials.authUrl,
197
- scryptSalt: credentials.scryptSalt,
198
- apiKey: credentials.apiKey,
199
- });
200
- sessionJWT = result.sessionJWT;
201
- // After session rotation, the previous capability JWT is also no longer
202
- // recognized by auth-service (challenge-status cache reset), so force
203
- // a refresh below.
204
- capabilityJWT = undefined;
205
- await writeJwtFile(args.sessionFile, sessionJWT);
206
- }
207
- const capabilityExpired = !capabilityJWT ||
208
- isJwtExpired(capabilityJWT, CAPABILITY_SAFETY_MARGIN_MS);
209
- if (capabilityExpired) {
210
- process.stderr.write("Capability JWT missing or near expiry — refreshing.\n");
211
- capabilityJWT = await fetchCapability(credentials.authUrl, sessionJWT);
212
- await writeJwtFile(args.capabilityFile, capabilityJWT);
213
- }
214
- return {
215
- capabilityJWT: capabilityJWT,
216
- sessionJWT: sessionJWT,
217
- credentials,
218
- };
219
- }
220
- async function main() {
221
- const args = readArgs();
222
- const envelope = await loadOps(args);
223
- if (args.dryRun) {
224
- const credentials = await readCredentials(args.credentialsFile);
225
- const url = args.sessionMode
226
- ? `${credentials.apiUrl}/.well-known/jmap`
227
- : `${credentials.apiUrl}/jmap/`;
228
- const dryRun = {
229
- method: args.sessionMode ? "GET" : "POST",
230
- url,
231
- headers: { Authorization: "Bearer <capability-jwt>" },
232
- body: envelope ?? null,
233
- };
234
- process.stdout.write(JSON.stringify(dryRun, null, 2) + "\n");
235
- return;
236
- }
237
- const tokens = await ensureFreshTokens(args);
238
- if (args.sessionMode) {
239
- const res = await fetch(`${tokens.credentials.apiUrl}/.well-known/jmap`, {
240
- headers: { Authorization: `Bearer ${tokens.capabilityJWT}` },
241
- });
242
- const body = await res.text();
243
- if (!res.ok) {
244
- fail(`JMAP session fetch failed (HTTP ${res.status}): ${body}`);
245
- }
246
- process.stdout.write(body.endsWith("\n") ? body : body + "\n");
247
- return;
248
- }
249
- const res = await fetch(`${tokens.credentials.apiUrl}/jmap/`, {
250
- method: "POST",
251
- headers: {
252
- "Content-Type": "application/json",
253
- Authorization: `Bearer ${tokens.capabilityJWT}`,
254
- },
255
- body: JSON.stringify(envelope),
256
- });
257
- const body = await res.text();
258
- if (!res.ok) {
259
- fail(`JMAP request failed (HTTP ${res.status}): ${body}`);
260
- }
261
- process.stdout.write(body.endsWith("\n") ? body : body + "\n");
262
- }
263
- main().catch((err) => {
264
- fail(err instanceof Error ? err.message : String(err));
265
- });
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../../src/skill/scripts/lib/auth.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,cAAc,QAAqB,CAAC;AACjD,eAAO,MAAM,iBAAiB,QAAgB,CAAC;AAI/C,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAClD,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAE/C,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,CAc/D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQnE;AAsCD,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACnC,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAU5C;AAiCD,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CAqBD;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IACJ,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACA,OAAO,CAAC,eAAe,CAAC,CAS1B;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAKD,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,eAAe,CAAC,CAgB1B"}
@@ -1,163 +0,0 @@
1
- // PoW + auth-service client used by signup.ts and jmap_request.ts.
2
- //
3
- // Mirrors services/auth-service/src/crypto.ts and the protocol implemented
4
- // in services/mcp-server-local/src/auth-session.ts. Uses only `node:` imports
5
- // so the same source runs on Deno, Node, and Bun unchanged.
6
- import { scrypt } from "node:crypto";
7
- const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };
8
- const POW_HASH_BYTES = 64;
9
- export const SESSION_TTL_MS = 4 * 60 * 60 * 1000;
10
- export const CAPABILITY_TTL_MS = 2 * 60 * 1000;
11
- // Refresh window: how close to expiry before we re-issue. The server enforces
12
- // strict expiry, so the client must rotate before the wire-side TTL elapses.
13
- export const CAPABILITY_SAFETY_MARGIN_MS = 20_000;
14
- export const SESSION_SAFETY_MARGIN_MS = 60_000;
15
- export function decodeJwtPayload(jwt) {
16
- const parts = jwt.split(".");
17
- if (parts.length < 2) {
18
- throw new Error("Malformed JWT: expected at least 2 dot-separated segments.");
19
- }
20
- const payloadB64Url = parts[1];
21
- const padLen = (4 - (payloadB64Url.length % 4)) % 4;
22
- const base64 = payloadB64Url
23
- .replace(/-/g, "+")
24
- .replace(/_/g, "/")
25
- .padEnd(payloadB64Url.length + padLen, "=");
26
- return JSON.parse(atob(base64));
27
- }
28
- export function isJwtExpired(jwt, marginMs) {
29
- try {
30
- const { exp } = decodeJwtPayload(jwt);
31
- if (typeof exp !== "number")
32
- return true;
33
- return Date.now() >= exp * 1000 - marginMs;
34
- }
35
- catch {
36
- return true;
37
- }
38
- }
39
- function bytesToHex(bytes) {
40
- let hex = "";
41
- for (let i = 0; i < bytes.length; i++) {
42
- hex += bytes[i].toString(16).padStart(2, "0");
43
- }
44
- return hex;
45
- }
46
- function hasLeadingZeroBits(hash, bits) {
47
- if (bits > hash.length * 8)
48
- return false;
49
- const fullBytes = Math.floor(bits / 8);
50
- const remainingBits = bits % 8;
51
- for (let i = 0; i < fullBytes; i++) {
52
- if (hash[i] !== 0)
53
- return false;
54
- }
55
- if (remainingBits > 0) {
56
- const mask = (0xff << (8 - remainingBits)) & 0xff;
57
- if ((hash[fullBytes] & mask) !== 0)
58
- return false;
59
- }
60
- return true;
61
- }
62
- // auth-service passes the SCRYPT_SALT_HEX string directly as the `salt`
63
- // argument to node:crypto's scrypt (i.e. the UTF-8 bytes of the hex string,
64
- // NOT the decoded hex bytes). We mirror that exactly so client and server
65
- // derive the same digest.
66
- function scryptHash(data, salt) {
67
- const bytes = new TextEncoder().encode(data);
68
- return new Promise((resolve, reject) => {
69
- scrypt(bytes, salt, POW_HASH_BYTES, SCRYPT_PARAMS, (err, derived) => {
70
- if (err)
71
- return reject(err);
72
- resolve(new Uint8Array(derived));
73
- });
74
- });
75
- }
76
- export async function solvePow(challenge, difficulty, salt, onProgress) {
77
- let nonce = 0n;
78
- while (true) {
79
- const digest = await scryptHash(`${challenge}:${nonce}`, salt);
80
- if (hasLeadingZeroBits(digest, difficulty)) {
81
- return { powHex: bytesToHex(digest), nonce: nonce.toString() };
82
- }
83
- nonce++;
84
- if (onProgress && nonce % 64n === 0n)
85
- onProgress(nonce);
86
- }
87
- }
88
- async function postJson(url, body, headers = {}) {
89
- const res = await fetch(url, {
90
- method: "POST",
91
- headers: {
92
- ...(body ? { "Content-Type": "application/json" } : {}),
93
- ...headers,
94
- },
95
- body: body ? JSON.stringify(body) : undefined,
96
- });
97
- const text = await res.text();
98
- const path = (() => {
99
- try {
100
- return new URL(url).pathname;
101
- }
102
- catch {
103
- return url;
104
- }
105
- })();
106
- if (!res.ok) {
107
- throw new Error(`auth-service ${path} returned ${res.status}: ${text}`);
108
- }
109
- try {
110
- return JSON.parse(text);
111
- }
112
- catch {
113
- throw new Error(`auth-service ${path} returned non-JSON body: ${text}`);
114
- }
115
- }
116
- export async function fetchChallenge(authUrl) {
117
- const data = await postJson(`${authUrl}/api/v1/challenge`, undefined);
118
- if (typeof data.challengeJWT !== "string") {
119
- throw new Error("Challenge response missing challengeJWT.");
120
- }
121
- const payload = decodeJwtPayload(data.challengeJWT);
122
- if (typeof payload.jti !== "string" ||
123
- typeof payload.difficulty !== "number") {
124
- throw new Error("Challenge JWT payload malformed (missing jti or difficulty).");
125
- }
126
- return {
127
- challengeJWT: data.challengeJWT,
128
- challenge: payload.jti,
129
- difficulty: payload.difficulty,
130
- };
131
- }
132
- export async function exchangeSession(authUrl, body) {
133
- const data = await postJson(`${authUrl}/api/v1/session`, { ...body });
134
- if (typeof data.sessionJWT !== "string") {
135
- throw new Error("Session response missing sessionJWT.");
136
- }
137
- return {
138
- sessionJWT: data.sessionJWT,
139
- apiKey: typeof data.apiKey === "string" ? data.apiKey : undefined,
140
- };
141
- }
142
- export async function fetchCapability(authUrl, sessionJWT) {
143
- const data = await postJson(`${authUrl}/api/v1/capability`, undefined, { Authorization: `Bearer ${sessionJWT}` });
144
- if (typeof data.capabilityJWT !== "string") {
145
- throw new Error("Capability response missing capabilityJWT.");
146
- }
147
- return data.capabilityJWT;
148
- }
149
- // One round trip of: /challenge -> solve PoW -> /session.
150
- // `apiKey` (login) and `username` (signup) are mutually exclusive; the caller
151
- // must enforce that before calling.
152
- export async function performPoWAndSession(input) {
153
- const { authUrl, scryptSalt } = input;
154
- const { challengeJWT, challenge, difficulty } = await fetchChallenge(authUrl);
155
- const { powHex, nonce } = await solvePow(challenge, difficulty, scryptSalt, input.onPowProgress);
156
- return exchangeSession(authUrl, {
157
- challengeJWT,
158
- powHex,
159
- nonce,
160
- apiKey: input.apiKey,
161
- username: input.username,
162
- });
163
- }
@@ -1 +0,0 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../../../src/skill/scripts/lib/credentials.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAOjE;AAMD,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAiCxE;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3E;AAED,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO7B"}
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
3
- //# sourceMappingURL=signup.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"signup.d.ts","sourceRoot":"","sources":["../../../src/skill/scripts/signup.ts"],"names":[],"mappings":""}