@itpay/cli 2.0.3 → 2.0.7

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 (77) hide show
  1. package/README.md +96 -142
  2. package/dist/src/client/backend.js +26 -8
  3. package/dist/src/client/http.js +29 -23
  4. package/dist/src/commands/buy.js +84 -132
  5. package/dist/src/commands/cart.js +274 -169
  6. package/dist/src/commands/catalog.js +64 -38
  7. package/dist/src/commands/checkout.js +128 -79
  8. package/dist/src/commands/docs.js +97 -51
  9. package/dist/src/commands/guidance.js +112 -16
  10. package/dist/src/commands/install.js +50 -87
  11. package/dist/src/commands/next.js +45 -0
  12. package/dist/src/commands/order.js +44 -69
  13. package/dist/src/commands/orders.js +43 -15
  14. package/dist/src/commands/pay.js +51 -22
  15. package/dist/src/commands/readyz.js +8 -4
  16. package/dist/src/commands/refund.js +132 -11
  17. package/dist/src/commands/services.js +799 -147
  18. package/dist/src/commands/skill.js +55 -0
  19. package/dist/src/main.js +820 -193
  20. package/dist/src/render/output.js +2 -3
  21. package/dist/src/state/agent_type.js +19 -0
  22. package/dist/src/state/cart_session.js +13 -17
  23. package/dist/src/state/client_context.js +4 -2
  24. package/dist/src/state/config.js +5 -15
  25. package/dist/src/state/device_authority.js +175 -57
  26. package/docs/agent/buyer/cart-checkout.json +27 -83
  27. package/docs/agent/buyer/catalog-list.json +2 -1
  28. package/docs/agent/buyer/identity-and-sessions.json +64 -0
  29. package/docs/agent/buyer/install-and-setup.json +35 -65
  30. package/docs/agent/buyer/orders-refunds.json +31 -53
  31. package/docs/agent/buyer/payment-flow.json +28 -57
  32. package/docs/agent/buyer/quickstart.json +46 -161
  33. package/docs/agent/buyer/render-hosts.json +43 -57
  34. package/docs/cli-reference/agent-types.md +51 -0
  35. package/docs/cli-reference/commands/buy.md +167 -0
  36. package/docs/cli-reference/commands/cart/add.md +86 -0
  37. package/docs/cli-reference/commands/cart/clear.md +53 -0
  38. package/docs/cli-reference/commands/cart/index.md +30 -0
  39. package/docs/cli-reference/commands/cart/next.md +71 -0
  40. package/docs/cli-reference/commands/cart/remove.md +53 -0
  41. package/docs/cli-reference/commands/cart/show.md +65 -0
  42. package/docs/cli-reference/commands/catalog/index.md +26 -0
  43. package/docs/cli-reference/commands/catalog/list.md +45 -0
  44. package/docs/cli-reference/commands/checkout.md +74 -0
  45. package/docs/cli-reference/commands/device.md +13 -0
  46. package/docs/cli-reference/commands/docs/index.md +28 -0
  47. package/docs/cli-reference/commands/docs/list.md +51 -0
  48. package/docs/cli-reference/commands/docs/search.md +69 -0
  49. package/docs/cli-reference/commands/docs/show.md +68 -0
  50. package/docs/cli-reference/commands/install.md +114 -0
  51. package/docs/cli-reference/commands/next.md +87 -0
  52. package/docs/cli-reference/commands/order.md +92 -0
  53. package/docs/cli-reference/commands/orders.md +83 -0
  54. package/docs/cli-reference/commands/pay.md +103 -0
  55. package/docs/cli-reference/commands/readyz.md +38 -0
  56. package/docs/cli-reference/commands/refund/cancel.md +62 -0
  57. package/docs/cli-reference/commands/refund/create.md +85 -0
  58. package/docs/cli-reference/commands/refund/get.md +60 -0
  59. package/docs/cli-reference/commands/refund/index.md +33 -0
  60. package/docs/cli-reference/commands/refund/list.md +68 -0
  61. package/docs/cli-reference/commands/refund/watch.md +73 -0
  62. package/docs/cli-reference/commands/services/action.md +48 -0
  63. package/docs/cli-reference/commands/services/checkout.md +82 -0
  64. package/docs/cli-reference/commands/services/events.md +73 -0
  65. package/docs/cli-reference/commands/services/get.md +66 -0
  66. package/docs/cli-reference/commands/services/index.md +45 -0
  67. package/docs/cli-reference/commands/services/invoke.md +67 -0
  68. package/docs/cli-reference/commands/services/list.md +61 -0
  69. package/docs/cli-reference/commands/services/next.md +181 -0
  70. package/docs/cli-reference/commands/services/quote.md +63 -0
  71. package/docs/cli-reference/commands/services/read-result.md +98 -0
  72. package/docs/cli-reference/commands/services/start.md +55 -0
  73. package/docs/cli-reference/commands/skill.md +17 -0
  74. package/docs/cli-reference/conventions.md +97 -0
  75. package/docs/cli-reference/index.md +65 -0
  76. package/package.json +1 -1
  77. package/skills/itpay-buyer/SKILL.md +71 -110
@@ -3,9 +3,6 @@ export function formatMoney(amountMinor, currency) {
3
3
  const major = (amountMinor / 100).toFixed(2);
4
4
  return `${major} ${currency}`;
5
5
  }
6
- export function renderReady(payload) {
7
- return `backend ${payload.status} (version ${payload.version})`;
8
- }
9
6
  export function renderOrder(order) {
10
7
  const lines = [];
11
8
  lines.push(`order ${order.order_id}`);
@@ -36,6 +33,8 @@ export function renderRefund(refund) {
36
33
  ` status: ${refund.status}`,
37
34
  ` amount: ${formatMoney(refund.amount_minor, refund.currency)}`,
38
35
  refund.reason ? ` reason: ${refund.reason}` : "",
36
+ ` access: ${refund.access_locked ? "locked" : "available"}`,
37
+ ` policy: ${refund.decision_mode === "automatic" ? "automatic" : "admin review"}`,
39
38
  ]
40
39
  .filter((line) => line.length > 0)
41
40
  .join("\n");
@@ -0,0 +1,19 @@
1
+ export function declaredAgentType(env = process.env, argv = process.argv) {
2
+ if (env.ITPAY_AGENT_TYPE)
3
+ return env.ITPAY_AGENT_TYPE;
4
+ for (let index = 0; index < argv.length; index += 1) {
5
+ const value = argv[index];
6
+ if (value === "--agent-type")
7
+ return argv[index + 1];
8
+ if (value?.startsWith("--agent-type="))
9
+ return value.slice("--agent-type=".length);
10
+ }
11
+ return undefined;
12
+ }
13
+ export function qualifyItPayCommand(command, agentType) {
14
+ if (!agentType || !/^[a-z0-9-]+$/.test(agentType))
15
+ return command;
16
+ if (!command.startsWith("itpay ") || /^itpay\s+--agent-type(?:=|\s)/.test(command))
17
+ return command;
18
+ return `itpay --agent-type ${agentType} ${command.slice("itpay ".length)}`;
19
+ }
@@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto";
7
7
  import { dirname } from "node:path";
8
8
  export class CartSession {
9
9
  state;
10
+ loadFailed = false;
10
11
  constructor(currency) {
11
12
  this.state = { currency, items: [] };
12
13
  }
@@ -23,8 +24,6 @@ export class CartSession {
23
24
  }
24
25
  });
25
26
  }
26
- if (persisted.agentDeviceID)
27
- session.state.agentDeviceID = persisted.agentDeviceID;
28
27
  if (persisted.lastCartID)
29
28
  session.state.lastCartID = persisted.lastCartID;
30
29
  if (persisted.lastCartItemID)
@@ -40,6 +39,7 @@ export class CartSession {
40
39
  }
41
40
  catch {
42
41
  session.state.items = [];
42
+ session.loadFailed = true;
43
43
  }
44
44
  }
45
45
  return session;
@@ -48,7 +48,6 @@ export class CartSession {
48
48
  const toSave = {
49
49
  currency: this.state.currency,
50
50
  items: this.state.items.map((item) => ({ ...item })),
51
- ...(this.state.agentDeviceID ? { agentDeviceID: this.state.agentDeviceID } : {}),
52
51
  ...(this.state.lastCartID ? { lastCartID: this.state.lastCartID } : {}),
53
52
  ...(this.state.lastCartItemID ? { lastCartItemID: this.state.lastCartItemID } : {}),
54
53
  ...(this.state.lastServiceExecutionID ? { lastServiceExecutionID: this.state.lastServiceExecutionID } : {}),
@@ -84,11 +83,7 @@ export class CartSession {
84
83
  }
85
84
  }
86
85
  clear() {
87
- this.state = {
88
- currency: this.state.currency,
89
- items: [],
90
- ...(this.state.agentDeviceID ? { agentDeviceID: this.state.agentDeviceID } : {}),
91
- };
86
+ this.state = { currency: this.state.currency, items: [] };
92
87
  }
93
88
  show() {
94
89
  return JSON.parse(JSON.stringify(this.state));
@@ -107,7 +102,8 @@ export class CartSession {
107
102
  }
108
103
  rememberCheckout(input) {
109
104
  this.state.items = [];
110
- this.state.lastCartID = input.cartID;
105
+ delete this.state.lastCartID;
106
+ delete this.state.lastCartItemID;
111
107
  this.state.lastCheckoutID = input.checkoutID;
112
108
  this.state.lastDisplayToken = input.displayToken;
113
109
  this.state.lastCheckoutURL = input.checkoutURL;
@@ -117,18 +113,15 @@ export class CartSession {
117
113
  rememberServerCart(input) {
118
114
  this.state.items = [];
119
115
  this.state.lastCartID = input.cartID;
116
+ delete this.state.lastCartItemID;
117
+ delete this.state.lastServiceExecutionID;
118
+ delete this.state.lastCheckoutID;
119
+ delete this.state.lastDisplayToken;
120
+ delete this.state.lastCheckoutURL;
120
121
  if (input.cartItemID)
121
122
  this.state.lastCartItemID = input.cartItemID;
122
123
  if (input.serviceExecutionID)
123
124
  this.state.lastServiceExecutionID = input.serviceExecutionID;
124
- if (input.agentDeviceID)
125
- this.state.agentDeviceID = input.agentDeviceID;
126
- }
127
- ensureAgentDeviceID(fallback) {
128
- if (!this.state.agentDeviceID) {
129
- this.state.agentDeviceID = fallback;
130
- }
131
- return this.state.agentDeviceID;
132
125
  }
133
126
  get lastCartID() {
134
127
  return this.state.lastCartID;
@@ -148,4 +141,7 @@ export class CartSession {
148
141
  get currency() {
149
142
  return this.state.currency;
150
143
  }
144
+ get stateLoadFailed() {
145
+ return this.loadFailed;
146
+ }
151
147
  }
@@ -56,10 +56,12 @@ export function hasDedicatedRenderer(host) {
56
56
  }
57
57
  export function defaultHostForAgentType(agentType) {
58
58
  const normalized = agentType?.trim().toLowerCase() ?? "";
59
- if (normalized.startsWith("codex"))
59
+ if (normalized === "codex-desktop")
60
60
  return "codex";
61
- if (normalized.startsWith("claude-code"))
61
+ if (normalized === "claude-code-desktop")
62
62
  return "claude-code";
63
+ if (normalized === "workbuddy")
64
+ return "plain-chat";
63
65
  return "terminal";
64
66
  }
65
67
  export function validateContext(host, target) {
@@ -6,11 +6,12 @@ import { mkdirSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
  import { HttpClient } from "../client/http.js";
8
8
  import { BackendClient } from "../client/backend.js";
9
+ import { declaredAgentType } from "./agent_type.js";
9
10
  import { DeviceAuthority } from "./device_authority.js";
10
11
  import { OperationJournal } from "./operation_journal.js";
11
12
  export const DEFAULT_BASE_URL = "https://app.itpay.ai";
12
- export const CLI_VERSION = "2.0.3";
13
- export const API_CONTRACT_REVISION = "sha256:d2870f3ee6fa4ee8a0a3ee75ae272a1a476935aae824123cf46607d62168f1f2";
13
+ export const CLI_VERSION = "2.0.7";
14
+ export const API_CONTRACT_REVISION = "sha256:47d42ab7bbe74a806b9ec989384b28ad715ffcf05a5eb913449b8bc224ffcf49";
14
15
  const CART_SESSION_DEFAULT_DIR = ".itpay-v3";
15
16
  const CART_SESSION_FILENAME = "cart.json";
16
17
  const OPERATION_JOURNAL_FILENAME = "operations.json";
@@ -25,15 +26,13 @@ export function cartSessionPath(env = process.env) {
25
26
  export function loadConfig(env = process.env) {
26
27
  const baseURL = env.ITPAY_BACKEND_URL || DEFAULT_BASE_URL;
27
28
  const bearerToken = env.ITPAY_BEARER_TOKEN || undefined;
28
- const agentDeviceID = env.ITPAY_AGENT_DEVICE_ID || "";
29
- const agentType = env.ITPAY_AGENT_TYPE || agentTypeFromArgv(process.argv);
29
+ const agentType = declaredAgentType(env);
30
30
  const checkoutCurrency = env.ITPAY_CURRENCY || "CNY";
31
31
  const idempotencyKey = env.ITPAY_IDEMPOTENCY_KEY || `cli_${shortRandom()}`;
32
32
  const ideImageAttach = env.ITPAY_IDE_IMAGE_ATTACH !== "0";
33
33
  const ideImageDirOverride = env.ITPAY_IDE_IMAGE_DIR_OVERRIDE;
34
34
  return {
35
35
  baseURL,
36
- agentDeviceID,
37
36
  ...(agentType ? { agentType } : {}),
38
37
  checkoutCurrency,
39
38
  idempotencyKey,
@@ -64,19 +63,10 @@ export function newBackendClient(config) {
64
63
  "X-ItPay-Contract-Revision": API_CONTRACT_REVISION,
65
64
  },
66
65
  requestAuthorizer: (input) => authority.authorizationHeaders(input),
66
+ recoverAuthorization: () => authority.recoverAuthorization(),
67
67
  });
68
68
  return new BackendClient(http);
69
69
  }
70
- function agentTypeFromArgv(argv) {
71
- for (let index = 0; index < argv.length; index += 1) {
72
- const value = argv[index];
73
- if (value === "--agent-type")
74
- return argv[index + 1];
75
- if (value?.startsWith("--agent-type="))
76
- return value.slice("--agent-type=".length);
77
- }
78
- return undefined;
79
- }
80
70
  function shortRandom() {
81
71
  return Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6);
82
72
  }
@@ -1,10 +1,11 @@
1
- import { createHash, createPrivateKey, generateKeyPairSync, randomUUID, sign, } from "node:crypto";
2
- import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign, } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, resolve } from "node:path";
5
- const PROTECTED_PATHS = ["/v1/carts", "/v1/service-executions", "/v1/agent-instances"];
5
+ const PROTECTED_PATHS = ["/v1/carts", "/v1/service-executions", "/v1/agent-instances", "/v1/orders", "/v1/refunds"];
6
6
  export class DeviceAuthority {
7
7
  baseURL;
8
+ backendKey;
8
9
  requestedAgentType;
9
10
  compatibilityHeaders;
10
11
  statePath;
@@ -13,6 +14,7 @@ export class DeviceAuthority {
13
14
  pending;
14
15
  constructor(options) {
15
16
  this.baseURL = options.baseURL.replace(/\/$/, "");
17
+ this.backendKey = normalizeBackendKey(options.baseURL);
16
18
  this.requestedAgentType = options.requestedAgentType;
17
19
  this.compatibilityHeaders = options.compatibilityHeaders;
18
20
  const root = resolve(homedir(), ".itpay-v3", "device");
@@ -47,54 +49,93 @@ export class DeviceAuthority {
47
49
  }
48
50
  return this.pending;
49
51
  }
52
+ async recoverAuthorization() {
53
+ await withFileLock(`${this.statePath}.lock`, async () => {
54
+ const state = this.readState();
55
+ if (!state || !this.requestedAgentType)
56
+ return;
57
+ const registration = state.registrations[this.backendKey];
58
+ if (!registration)
59
+ return;
60
+ delete registration.sessions[this.requestedAgentType];
61
+ this.writeState(state);
62
+ });
63
+ }
64
+ async recoverBackendReset() {
65
+ return withFileLock(`${this.statePath}.lock`, async () => {
66
+ const state = this.readState();
67
+ const registration = state?.registrations[this.backendKey];
68
+ if (!state || !registration)
69
+ return { removed: false, agentTypes: [] };
70
+ const agentTypes = Object.keys(registration.agentInstances).sort();
71
+ delete state.registrations[this.backendKey];
72
+ this.writeState(state);
73
+ return { removed: true, agentTypes };
74
+ });
75
+ }
50
76
  async prepareAuthorization() {
51
- let state = this.readState();
52
- const agentType = this.requestedAgentType ?? firstAgentType(state);
77
+ let state = this.readState() ?? emptyDeviceState();
78
+ const agentType = this.requestedAgentType;
53
79
  if (!agentType) {
54
80
  throw new Error("agent type is required for ItPay commerce; pass --agent-type <type> or set ITPAY_AGENT_TYPE");
55
81
  }
56
82
  let privateKey = this.readPrivateKey();
57
- if (!state || !privateKey) {
58
- const enrolled = await this.enroll(agentType);
59
- state = enrolled.state;
60
- privateKey = enrolled.privateKey;
83
+ if (!privateKey) {
84
+ const pair = generateKeyPairSync("ed25519");
85
+ privateKey = pair.privateKey;
86
+ this.writePrivateKey(pair.privateKey.export({ format: "pem", type: "pkcs8" }).toString());
87
+ state = emptyDeviceState();
61
88
  }
62
- if (!state.agentInstances[agentType]) {
63
- const existingType = firstAgentType(state);
89
+ let registration = state.registrations[this.backendKey];
90
+ if (!registration && state.legacyRegistration) {
91
+ try {
92
+ await this.ensureRegistrationAgentType(state.legacyRegistration, agentType, privateKey, true);
93
+ registration = state.legacyRegistration;
94
+ delete state.legacyRegistration;
95
+ }
96
+ catch (error) {
97
+ if (!canMovePastLegacyRegistration(error))
98
+ throw error;
99
+ }
100
+ }
101
+ if (!registration) {
102
+ registration = await this.enroll(agentType, privateKey);
103
+ }
104
+ state.registrations[this.backendKey] = registration;
105
+ const session = await this.ensureRegistrationAgentType(registration, agentType, privateKey, false);
106
+ this.writeState(state);
107
+ return { state: registration, agentType, session, privateKey };
108
+ }
109
+ async ensureRegistrationAgentType(registration, agentType, privateKey, forceSession) {
110
+ if (!registration.agentInstances[agentType]) {
111
+ const existingType = firstAgentType(registration);
64
112
  if (!existingType)
65
113
  throw new Error("device has no registered agent instance");
66
- const existingSession = await this.ensureSession(state, existingType, privateKey);
67
- const registered = await this.signedJSON("/v1/agent-instances", { agent_type: agentType }, state, existingType, existingSession, privateKey);
68
- state.agentInstances[agentType] = registered.agent_instance_id;
69
- this.writeState(state);
114
+ const existingSession = await this.ensureSession(registration, existingType, privateKey, forceSession);
115
+ const registered = await this.signedJSON("/v1/agent-instances", { agent_type: agentType }, registration, existingType, existingSession, privateKey);
116
+ registration.agentInstances[agentType] = registered.agent_instance_id;
70
117
  }
71
- const session = await this.ensureSession(state, agentType, privateKey);
72
- return { state, agentType, session, privateKey };
118
+ return this.ensureSession(registration, agentType, privateKey, forceSession);
73
119
  }
74
- async enroll(agentType) {
75
- const pair = generateKeyPairSync("ed25519");
76
- const publicJWK = pair.publicKey.export({ format: "jwk" });
120
+ async enroll(agentType, privateKey) {
121
+ const publicJWK = createPublicKey(privateKey).export({ format: "jwk" });
77
122
  if (!publicJWK.x)
78
123
  throw new Error("unable to export Ed25519 public key");
79
124
  const publicKey = Buffer.from(publicJWK.x, "base64url").toString("base64");
80
125
  const started = await this.publicJSON("/v1/agent-device-enrollments", { public_key: publicKey, agent_type: agentType });
81
126
  const proof = enrollmentProofMessage(started.agent_device_enrollment_id, started.challenge);
82
- const verified = await this.publicJSON(`/v1/agent-device-enrollments/${encodeURIComponent(started.agent_device_enrollment_id)}/verify`, { challenge: started.challenge, signature: sign(null, Buffer.from(proof), pair.privateKey).toString("base64") });
83
- const state = {
84
- schemaVersion: "itpay.device.v1",
127
+ const verified = await this.publicJSON(`/v1/agent-device-enrollments/${encodeURIComponent(started.agent_device_enrollment_id)}/verify`, { challenge: started.challenge, signature: sign(null, Buffer.from(proof), privateKey).toString("base64") });
128
+ return {
85
129
  deviceID: verified.agent_device_id,
86
130
  deviceKeyID: verified.agent_device_key_id,
87
131
  quotaLineageID: verified.quota_lineage_id,
88
132
  agentInstances: { [verified.agent_type]: verified.agent_instance_id },
89
133
  sessions: {},
90
134
  };
91
- this.writePrivateKey(pair.privateKey.export({ format: "pem", type: "pkcs8" }).toString());
92
- this.writeState(state);
93
- return { state, privateKey: pair.privateKey };
94
135
  }
95
- async ensureSession(state, agentType, privateKey) {
136
+ async ensureSession(state, agentType, privateKey, force = false) {
96
137
  const existing = state.sessions[agentType];
97
- if (existing && Date.parse(existing.expiresAt) > Date.now() + 60_000)
138
+ if (!force && existing && Date.parse(existing.expiresAt) > Date.now() + 60_000)
98
139
  return existing;
99
140
  const instanceID = state.agentInstances[agentType];
100
141
  if (!instanceID)
@@ -107,7 +148,6 @@ export class DeviceAuthority {
107
148
  const verified = await this.publicJSON(`/v1/agent-device-session-challenges/${encodeURIComponent(challenge.agent_device_session_challenge_id)}/verify`, { challenge: challenge.challenge, signature: sign(null, Buffer.from(proof), privateKey).toString("base64") });
108
149
  const session = { token: verified.session_token, expiresAt: verified.expires_at };
109
150
  state.sessions[agentType] = session;
110
- this.writeState(state);
111
151
  return session;
112
152
  }
113
153
  async signedJSON(path, bodyValue, state, agentType, session, privateKey) {
@@ -137,16 +177,26 @@ export class DeviceAuthority {
137
177
  });
138
178
  const payload = await response.json().catch(() => ({}));
139
179
  if (!response.ok)
140
- throw new Error(payload.message || payload.code || `ItPay device request failed: ${response.status}`);
180
+ throw new DeviceAuthorizationError(response.status, payload.code, payload.message || payload.code || `ItPay device request failed: ${response.status}`);
141
181
  return payload;
142
182
  }
143
183
  readState() {
144
184
  if (!existsSync(this.statePath))
145
185
  return undefined;
146
186
  try {
147
- return JSON.parse(readFileSync(this.statePath, "utf8"));
187
+ const parsed = JSON.parse(readFileSync(this.statePath, "utf8"));
188
+ if (parsed.schemaVersion === "itpay.device.v2")
189
+ return parsed;
190
+ if (parsed.schemaVersion === "itpay.device.v1") {
191
+ const { schemaVersion: _, ...legacyRegistration } = parsed;
192
+ return { ...emptyDeviceState(), legacyRegistration };
193
+ }
194
+ return undefined;
148
195
  }
149
- catch {
196
+ catch (error) {
197
+ const stateError = asDeviceStateError(error, "read_state");
198
+ if (stateError)
199
+ throw stateError;
150
200
  return undefined;
151
201
  }
152
202
  }
@@ -156,62 +206,130 @@ export class DeviceAuthority {
156
206
  try {
157
207
  return createPrivateKey(readFileSync(this.privateKeyPath, "utf8"));
158
208
  }
159
- catch {
209
+ catch (error) {
210
+ const stateError = asDeviceStateError(error, "read_private_key");
211
+ if (stateError)
212
+ throw stateError;
160
213
  return undefined;
161
214
  }
162
215
  }
163
- writeState(state) { atomicOwnerOnlyWrite(this.statePath, JSON.stringify(state, null, 2)); }
164
- writePrivateKey(value) { atomicOwnerOnlyWrite(this.privateKeyPath, value); }
216
+ writeState(state) { atomicOwnerOnlyWrite(this.statePath, JSON.stringify(state, null, 2), "write_state"); }
217
+ writePrivateKey(value) { atomicOwnerOnlyWrite(this.privateKeyPath, value, "write_private_key"); }
218
+ }
219
+ export class DeviceAuthorizationError extends Error {
220
+ status;
221
+ code;
222
+ constructor(status, code, message) {
223
+ super(message);
224
+ this.status = status;
225
+ this.code = code;
226
+ this.name = "DeviceAuthorizationError";
227
+ }
228
+ }
229
+ export class DeviceStateError extends Error {
230
+ operation;
231
+ causeCode;
232
+ code = "device_state_unwritable";
233
+ constructor(operation, causeCode) {
234
+ super(`ItPay device state operation failed: ${operation} (${causeCode})`);
235
+ this.operation = operation;
236
+ this.causeCode = causeCode;
237
+ this.name = "DeviceStateError";
238
+ }
239
+ }
240
+ function emptyDeviceState() {
241
+ return { schemaVersion: "itpay.device.v2", registrations: {} };
242
+ }
243
+ function firstAgentType(state) { return Object.keys(state.agentInstances)[0]; }
244
+ function canMovePastLegacyRegistration(error) {
245
+ return error instanceof DeviceAuthorizationError && (error.code === "agent_device_revoked" || error.status === 404);
246
+ }
247
+ function normalizeBackendKey(value) {
248
+ const url = new URL(value);
249
+ url.search = "";
250
+ url.hash = "";
251
+ url.pathname = url.pathname.replace(/\/+$/, "");
252
+ return url.toString().replace(/\/$/, "");
165
253
  }
166
- function firstAgentType(state) { return state ? Object.keys(state.agentInstances)[0] : undefined; }
167
254
  function sha256(value) { return `sha256:${createHash("sha256").update(value).digest("hex")}`; }
168
255
  function enrollmentProofMessage(id, challenge) { return `itpay-device-enrollment/v1\n${id}\n${challenge}`; }
169
256
  function deviceSessionProofMessage(id, challenge) { return `itpay-device-session/v1\n${id}\n${challenge}`; }
170
257
  function requestProofMessage(method, path, bodyHash, timestamp, jti) { return ["itpay-agent-request/v1", method, path, bodyHash, timestamp, jti].join("\n"); }
171
- function atomicOwnerOnlyWrite(path, value) {
172
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
258
+ function atomicOwnerOnlyWrite(path, value, operation) {
173
259
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
174
- writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600 });
175
- chmodSync(temporary, 0o600);
176
- renameSync(temporary, path);
177
- chmodSync(path, 0o600);
260
+ try {
261
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
262
+ writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600 });
263
+ chmodSync(temporary, 0o600);
264
+ renameSync(temporary, path);
265
+ chmodSync(path, 0o600);
266
+ }
267
+ catch (error) {
268
+ try {
269
+ unlinkSync(temporary);
270
+ }
271
+ catch { /* best-effort cleanup */ }
272
+ throw asDeviceStatePathError(error, operation) ?? error;
273
+ }
178
274
  }
179
275
  async function withFileLock(path, run) {
180
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
181
- let descriptor;
276
+ try {
277
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
278
+ }
279
+ catch (error) {
280
+ throw asDeviceStatePathError(error, "prepare_lock") ?? error;
281
+ }
282
+ let acquired = false;
182
283
  for (let attempt = 0; attempt < 200; attempt += 1) {
183
284
  try {
184
- descriptor = openSync(path, "wx", 0o600);
285
+ mkdirSync(path, { mode: 0o700 });
286
+ acquired = true;
185
287
  break;
186
288
  }
187
289
  catch (error) {
188
290
  const code = error.code;
189
291
  if (code !== "EEXIST")
190
- throw error;
292
+ throw asDeviceStateError(error, "acquire_lock") ?? error;
191
293
  try {
192
294
  if (Date.now() - statSync(path).mtimeMs > 30_000)
193
- unlinkSync(path);
295
+ removeLock(path, "remove_stale_lock");
194
296
  }
195
297
  catch (statError) {
196
- if (statError.code !== "ENOENT")
197
- throw statError;
298
+ if (statError.code !== "ENOENT") {
299
+ throw asDeviceStateError(statError, "inspect_lock") ?? statError;
300
+ }
198
301
  }
199
302
  await new Promise((resolve) => setTimeout(resolve, 25));
200
303
  }
201
304
  }
202
- if (descriptor === undefined)
305
+ if (!acquired)
203
306
  throw new Error("timed out waiting for ItPay device identity lock");
204
307
  try {
205
308
  return await run();
206
309
  }
207
310
  finally {
208
- closeSync(descriptor);
209
- try {
311
+ removeLock(path, "release_lock");
312
+ }
313
+ }
314
+ function removeLock(path, operation) {
315
+ try {
316
+ if (statSync(path).isDirectory())
317
+ rmdirSync(path);
318
+ else
210
319
  unlinkSync(path);
211
- }
212
- catch (error) {
213
- if (error.code !== "ENOENT")
214
- throw error;
215
- }
216
320
  }
321
+ catch (error) {
322
+ if (error.code !== "ENOENT")
323
+ throw asDeviceStateError(error, operation) ?? error;
324
+ }
325
+ }
326
+ function asDeviceStateError(error, operation) {
327
+ const code = error.code;
328
+ return code === "EACCES" || code === "EPERM" || code === "EROFS" || code === "ENOTDIR" || code === "EISDIR"
329
+ ? new DeviceStateError(operation, code)
330
+ : undefined;
331
+ }
332
+ function asDeviceStatePathError(error, operation) {
333
+ const code = error.code;
334
+ return code === "EEXIST" ? new DeviceStateError(operation, code) : asDeviceStateError(error, operation);
217
335
  }