workos 0.15.2 → 0.16.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.
Files changed (107) hide show
  1. package/README.md +37 -11
  2. package/dist/bin.js +1439 -1257
  3. package/dist/bin.js.map +1 -1
  4. package/dist/cli.config.d.ts +1 -0
  5. package/dist/cli.config.js +1 -0
  6. package/dist/cli.config.js.map +1 -1
  7. package/dist/commands/api/index.js +7 -2
  8. package/dist/commands/api/index.js.map +1 -1
  9. package/dist/commands/api/interactive.js +9 -3
  10. package/dist/commands/api/interactive.js.map +1 -1
  11. package/dist/commands/debug.d.ts +2 -1
  12. package/dist/commands/debug.js +43 -3
  13. package/dist/commands/debug.js.map +1 -1
  14. package/dist/commands/dev.js +5 -4
  15. package/dist/commands/dev.js.map +1 -1
  16. package/dist/commands/doctor.js +13 -4
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/emulate.js +2 -2
  19. package/dist/commands/emulate.js.map +1 -1
  20. package/dist/commands/env.js +5 -4
  21. package/dist/commands/env.js.map +1 -1
  22. package/dist/commands/install-skill.js +4 -3
  23. package/dist/commands/install-skill.js.map +1 -1
  24. package/dist/commands/install.js +2 -2
  25. package/dist/commands/install.js.map +1 -1
  26. package/dist/commands/login.js +3 -3
  27. package/dist/commands/login.js.map +1 -1
  28. package/dist/commands/migrations.d.ts +1 -1
  29. package/dist/commands/migrations.js +4 -1
  30. package/dist/commands/migrations.js.map +1 -1
  31. package/dist/commands/telemetry.d.ts +3 -0
  32. package/dist/commands/telemetry.js +88 -0
  33. package/dist/commands/telemetry.js.map +1 -0
  34. package/dist/commands/uninstall-skill.js +3 -2
  35. package/dist/commands/uninstall-skill.js.map +1 -1
  36. package/dist/commands/vault-run.d.ts +13 -0
  37. package/dist/commands/vault-run.js +194 -0
  38. package/dist/commands/vault-run.js.map +1 -0
  39. package/dist/commands/vault.d.ts +3 -2
  40. package/dist/commands/vault.js +41 -8
  41. package/dist/commands/vault.js.map +1 -1
  42. package/dist/lib/api-error-handler.d.ts +15 -3
  43. package/dist/lib/api-error-handler.js +52 -34
  44. package/dist/lib/api-error-handler.js.map +1 -1
  45. package/dist/lib/command-aliases.d.ts +8 -0
  46. package/dist/lib/command-aliases.js +12 -0
  47. package/dist/lib/command-aliases.js.map +1 -0
  48. package/dist/lib/constants.d.ts +0 -1
  49. package/dist/lib/constants.js +0 -1
  50. package/dist/lib/constants.js.map +1 -1
  51. package/dist/lib/device-id.d.ts +25 -0
  52. package/dist/lib/device-id.js +102 -0
  53. package/dist/lib/device-id.js.map +1 -0
  54. package/dist/lib/preferences.d.ts +101 -0
  55. package/dist/lib/preferences.js +198 -0
  56. package/dist/lib/preferences.js.map +1 -0
  57. package/dist/lib/run-with-core.js +15 -15
  58. package/dist/lib/run-with-core.js.map +1 -1
  59. package/dist/lib/settings.d.ts +6 -0
  60. package/dist/lib/settings.js +7 -0
  61. package/dist/lib/settings.js.map +1 -1
  62. package/dist/lib/telemetry-notice.d.ts +25 -0
  63. package/dist/lib/telemetry-notice.js +56 -0
  64. package/dist/lib/telemetry-notice.js.map +1 -0
  65. package/dist/test/force-insecure-storage.d.ts +1 -0
  66. package/dist/test/force-insecure-storage.js +9 -0
  67. package/dist/test/force-insecure-storage.js.map +1 -0
  68. package/dist/test/setup.d.ts +1 -0
  69. package/dist/test/setup.js +38 -0
  70. package/dist/test/setup.js.map +1 -0
  71. package/dist/utils/analytics.d.ts +41 -0
  72. package/dist/utils/analytics.js +199 -12
  73. package/dist/utils/analytics.js.map +1 -1
  74. package/dist/utils/box.d.ts +29 -1
  75. package/dist/utils/box.js +92 -4
  76. package/dist/utils/box.js.map +1 -1
  77. package/dist/utils/cli-exit.d.ts +15 -0
  78. package/dist/utils/cli-exit.js +11 -0
  79. package/dist/utils/cli-exit.js.map +1 -0
  80. package/dist/utils/cli-symbols.d.ts +1 -1
  81. package/dist/utils/command-telemetry.d.ts +17 -0
  82. package/dist/utils/command-telemetry.js +67 -0
  83. package/dist/utils/command-telemetry.js.map +1 -0
  84. package/dist/utils/crash-reporter.d.ts +13 -0
  85. package/dist/utils/crash-reporter.js +91 -0
  86. package/dist/utils/crash-reporter.js.map +1 -0
  87. package/dist/utils/debug.d.ts +1 -0
  88. package/dist/utils/debug.js +4 -1
  89. package/dist/utils/debug.js.map +1 -1
  90. package/dist/utils/exit-codes.d.ts +5 -0
  91. package/dist/utils/exit-codes.js +30 -1
  92. package/dist/utils/exit-codes.js.map +1 -1
  93. package/dist/utils/help-json.d.ts +6 -0
  94. package/dist/utils/help-json.js +87 -10
  95. package/dist/utils/help-json.js.map +1 -1
  96. package/dist/utils/output.d.ts +7 -2
  97. package/dist/utils/output.js +9 -2
  98. package/dist/utils/output.js.map +1 -1
  99. package/dist/utils/telemetry-client.d.ts +30 -2
  100. package/dist/utils/telemetry-client.js +122 -12
  101. package/dist/utils/telemetry-client.js.map +1 -1
  102. package/dist/utils/telemetry-store-forward.d.ts +11 -0
  103. package/dist/utils/telemetry-store-forward.js +94 -0
  104. package/dist/utils/telemetry-store-forward.js.map +1 -0
  105. package/dist/utils/telemetry-types.d.ts +58 -9
  106. package/dist/utils/telemetry-types.js.map +1 -1
  107. package/package.json +1 -1
@@ -1,12 +1,36 @@
1
- import { debug } from './debug.js';
2
- import { getCredentials } from '../lib/credentials.js';
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { debug, isDebugEnabled } from './debug.js';
4
+ import { getCredentials, isTokenExpired } from '../lib/credentials.js';
5
+ function summarizeEvent(event) {
6
+ switch (event.type) {
7
+ case 'session.start':
8
+ return `session.start(mode=${event.attributes['installer.mode']}, os=${event.attributes['env.os']})`;
9
+ case 'session.end':
10
+ return `session.end(outcome=${event.attributes['installer.outcome']}, duration=${event.attributes['installer.duration_ms']}ms)`;
11
+ case 'step':
12
+ return `step(${event.name}, ${event.durationMs}ms, success=${event.success})`;
13
+ case 'agent.tool':
14
+ return `agent.tool(${event.toolName}, ${event.durationMs}ms)`;
15
+ case 'agent.llm':
16
+ return `agent.llm(${event.model}, in=${event.inputTokens}, out=${event.outputTokens})`;
17
+ case 'command':
18
+ return `command(${event.attributes['command.name']}, ${event.attributes['command.duration_ms']}ms, success=${event.attributes['command.success']})`;
19
+ case 'crash':
20
+ return `crash(${event.attributes['crash.error_type']}: ${event.attributes['crash.error_message']})`;
21
+ }
22
+ }
3
23
  /**
4
- * HTTP client that queues telemetry events and flushes them to the gateway.
24
+ * HTTP client that queues telemetry events and flushes them to the API.
5
25
  * Failures are silent—telemetry should never crash the wizard.
6
26
  */
7
27
  export class TelemetryClient {
8
28
  events = [];
29
+ flushInFlight = null;
9
30
  accessToken = null;
31
+ claimToken = null;
32
+ clientId = null;
33
+ apiKey = null;
10
34
  gatewayUrl = null;
11
35
  setGatewayUrl(url) {
12
36
  this.gatewayUrl = url;
@@ -14,48 +38,134 @@ export class TelemetryClient {
14
38
  setAccessToken(token) {
15
39
  this.accessToken = token;
16
40
  }
41
+ setApiKeyAuth(apiKey) {
42
+ this.apiKey = apiKey;
43
+ }
44
+ /**
45
+ * Set claim-token auth for unclaimed environments.
46
+ * The API accepts either a JWT (Bearer), claim token
47
+ * (x-workos-claim-token + x-workos-client-id), or API key
48
+ * (x-workos-api-key).
49
+ */
50
+ setClaimTokenAuth(clientId, claimToken) {
51
+ this.clientId = clientId;
52
+ this.claimToken = claimToken;
53
+ }
17
54
  queueEvent(event) {
18
55
  this.events.push(event);
19
56
  }
57
+ /**
58
+ * Queue multiple pre-formed events (used by store-forward recovery).
59
+ */
60
+ queueEvents(events) {
61
+ this.events.push(...events);
62
+ }
63
+ /**
64
+ * Flush queued events. Returns true if events were sent or intentionally
65
+ * dropped (4xx), false if they should be retried (5xx/network error).
66
+ * Uses splice to only remove the events that were in the snapshot,
67
+ * protecting any events queued concurrently during the fetch.
68
+ */
20
69
  async flush() {
70
+ // Coalesce overlapping flushes: a second caller during an in-flight flush
71
+ // would otherwise snapshot and POST the same events again (duplicate send),
72
+ // and its splice() could drop events queued after the first flush started.
73
+ if (this.flushInFlight)
74
+ return this.flushInFlight;
75
+ this.flushInFlight = this.flushInternal();
76
+ try {
77
+ return await this.flushInFlight;
78
+ }
79
+ finally {
80
+ this.flushInFlight = null;
81
+ }
82
+ }
83
+ async flushInternal() {
21
84
  if (this.events.length === 0)
22
- return;
85
+ return true;
23
86
  if (!this.gatewayUrl) {
24
- debug('[Telemetry] No gateway URL configured, skipping flush');
25
- return;
87
+ debug('[Telemetry] No telemetry URL configured, skipping flush');
88
+ return false;
26
89
  }
27
- const payload = { events: [...this.events] };
28
- this.events = [];
90
+ const count = this.events.length;
91
+ const payload = { events: this.events.slice(0, count) };
29
92
  const headers = {
30
93
  'Content-Type': 'application/json',
31
94
  };
32
- // Read fresh credentials to handle token refresh mid-session
95
+ // Read fresh credentials to handle token refresh mid-session. Skip an
96
+ // expired stored token — sending a dead Bearer 401s and the event is
97
+ // dropped, so fall through to claim-token / api-key auth instead.
33
98
  const freshCreds = getCredentials();
34
- const token = freshCreds?.accessToken ?? this.accessToken;
99
+ const token = freshCreds?.accessToken
100
+ ? isTokenExpired(freshCreds)
101
+ ? null
102
+ : freshCreds.accessToken
103
+ : this.accessToken;
35
104
  if (token) {
36
105
  headers['Authorization'] = `Bearer ${token}`;
37
106
  }
107
+ else if (this.claimToken && this.clientId) {
108
+ // Unclaimed environment auth path — guard accepts this instead of JWT
109
+ headers['x-workos-claim-token'] = this.claimToken;
110
+ headers['x-workos-client-id'] = this.clientId;
111
+ }
112
+ else if (this.apiKey) {
113
+ headers['x-workos-api-key'] = this.apiKey;
114
+ }
38
115
  const controller = new AbortController();
39
116
  const timeout = setTimeout(() => controller.abort(), 3000);
40
117
  try {
41
- debug(`[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry`);
118
+ if (isDebugEnabled()) {
119
+ const eventSummary = payload.events.map(summarizeEvent).join('\n ');
120
+ debug(`[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry:\n ${eventSummary}`);
121
+ }
42
122
  const response = await fetch(`${this.gatewayUrl}/telemetry`, {
43
123
  method: 'POST',
44
124
  headers,
45
125
  body: JSON.stringify(payload),
46
126
  signal: controller.signal,
47
127
  });
48
- if (!response.ok) {
128
+ if (response.ok) {
129
+ this.events.splice(0, count);
130
+ return true;
131
+ }
132
+ else {
49
133
  debug(`[Telemetry] Failed to send: ${response.status}`);
134
+ // Drop on 4xx (permanent failures like 401/403 won't succeed on retry).
135
+ // Retain on 5xx (transient server errors) for store-forward.
136
+ if (response.status >= 400 && response.status < 500) {
137
+ this.events.splice(0, count);
138
+ return true; // intentionally dropped
139
+ }
140
+ return false;
50
141
  }
51
142
  }
52
143
  catch (error) {
53
144
  debug(`[Telemetry] Error sending events: ${error}`);
145
+ // Events remain in queue for store-forward to persist
146
+ return false;
54
147
  }
55
148
  finally {
56
149
  clearTimeout(timeout);
57
150
  }
58
151
  }
152
+ /**
153
+ * Synchronously write pending events to a file.
154
+ * Used as last resort in process.on('exit') handler.
155
+ */
156
+ persistToFile(filePath) {
157
+ if (this.events.length === 0)
158
+ return;
159
+ try {
160
+ // Restrictive modes — the payload carries device/user identifiers.
161
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
162
+ writeFileSync(filePath, JSON.stringify(this.events), { encoding: 'utf-8', mode: 0o600 });
163
+ this.events = [];
164
+ }
165
+ catch {
166
+ // Silent failure — telemetry must never block exit
167
+ }
168
+ }
59
169
  }
60
170
  export const telemetryClient = new TelemetryClient();
61
171
  //# sourceMappingURL=telemetry-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry-client.js","sourceRoot":"","sources":["../../src/utils/telemetry-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;GAGG;AACH,MAAM,OAAO,eAAe;IAClB,MAAM,GAAqB,EAAE,CAAC;IAC9B,WAAW,GAAkB,IAAI,CAAC;IAClC,UAAU,GAAkB,IAAI,CAAC;IAEzC,aAAa,CAAC,GAAW;QACvB,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;IACxB,CAAC;IAED,cAAc,CAAC,KAAa;QAC1B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;IAC3B,CAAC;IAED,UAAU,CAAC,KAAqB;QAC9B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACrC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,KAAK,CAAC,uDAAuD,CAAC,CAAC;YAC/D,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAqB,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/D,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAEjB,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;SACnC,CAAC;QACF,6DAA6D;QAC7D,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,UAAU,EAAE,WAAW,IAAI,IAAI,CAAC,WAAW,CAAC;QAC1D,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAC;QAC/C,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;QAE3D,IAAI,CAAC;YACH,KAAK,CAAC,uBAAuB,OAAO,CAAC,MAAM,CAAC,MAAM,cAAc,IAAI,CAAC,UAAU,YAAY,CAAC,CAAC;YAE7F,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,UAAU,YAAY,EAAE;gBAC3D,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;gBAC7B,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,KAAK,CAAC,+BAA+B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,CAAC,qCAAqC,KAAK,EAAE,CAAC,CAAC;QACtD,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;CACF;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC","sourcesContent":["import { debug } from './debug.js';\nimport type { TelemetryEvent, TelemetryRequest } from './telemetry-types.js';\nimport { getCredentials } from '../lib/credentials.js';\n\n/**\n * HTTP client that queues telemetry events and flushes them to the gateway.\n * Failures are silent—telemetry should never crash the wizard.\n */\nexport class TelemetryClient {\n private events: TelemetryEvent[] = [];\n private accessToken: string | null = null;\n private gatewayUrl: string | null = null;\n\n setGatewayUrl(url: string) {\n this.gatewayUrl = url;\n }\n\n setAccessToken(token: string) {\n this.accessToken = token;\n }\n\n queueEvent(event: TelemetryEvent) {\n this.events.push(event);\n }\n\n async flush(): Promise<void> {\n if (this.events.length === 0) return;\n if (!this.gatewayUrl) {\n debug('[Telemetry] No gateway URL configured, skipping flush');\n return;\n }\n\n const payload: TelemetryRequest = { events: [...this.events] };\n this.events = [];\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n // Read fresh credentials to handle token refresh mid-session\n const freshCreds = getCredentials();\n const token = freshCreds?.accessToken ?? this.accessToken;\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000);\n\n try {\n debug(`[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry`);\n\n const response = await fetch(`${this.gatewayUrl}/telemetry`, {\n method: 'POST',\n headers,\n body: JSON.stringify(payload),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n debug(`[Telemetry] Failed to send: ${response.status}`);\n }\n } catch (error) {\n debug(`[Telemetry] Error sending events: ${error}`);\n } finally {\n clearTimeout(timeout);\n }\n }\n}\n\nexport const telemetryClient = new TelemetryClient();\n"]}
1
+ {"version":3,"file":"telemetry-client.js","sourceRoot":"","sources":["../../src/utils/telemetry-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { debug, isDebugEnabled } from './debug.js';\nimport type { TelemetryEvent, TelemetryRequest } from './telemetry-types.js';\nimport { getCredentials, isTokenExpired } from '../lib/credentials.js';\n\nfunction summarizeEvent(event: TelemetryEvent): string {\n switch (event.type) {\n case 'session.start':\n return `session.start(mode=${event.attributes['installer.mode']}, os=${event.attributes['env.os']})`;\n case 'session.end':\n return `session.end(outcome=${event.attributes['installer.outcome']}, duration=${event.attributes['installer.duration_ms']}ms)`;\n case 'step':\n return `step(${event.name}, ${event.durationMs}ms, success=${event.success})`;\n case 'agent.tool':\n return `agent.tool(${event.toolName}, ${event.durationMs}ms)`;\n case 'agent.llm':\n return `agent.llm(${event.model}, in=${event.inputTokens}, out=${event.outputTokens})`;\n case 'command':\n return `command(${event.attributes['command.name']}, ${event.attributes['command.duration_ms']}ms, success=${event.attributes['command.success']})`;\n case 'crash':\n return `crash(${event.attributes['crash.error_type']}: ${event.attributes['crash.error_message']})`;\n }\n}\n\n/**\n * HTTP client that queues telemetry events and flushes them to the API.\n * Failures are silent—telemetry should never crash the wizard.\n */\nexport class TelemetryClient {\n private events: TelemetryEvent[] = [];\n private flushInFlight: Promise<boolean> | null = null;\n private accessToken: string | null = null;\n private claimToken: string | null = null;\n private clientId: string | null = null;\n private apiKey: string | null = null;\n private gatewayUrl: string | null = null;\n\n setGatewayUrl(url: string) {\n this.gatewayUrl = url;\n }\n\n setAccessToken(token: string) {\n this.accessToken = token;\n }\n\n setApiKeyAuth(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n /**\n * Set claim-token auth for unclaimed environments.\n * The API accepts either a JWT (Bearer), claim token\n * (x-workos-claim-token + x-workos-client-id), or API key\n * (x-workos-api-key).\n */\n setClaimTokenAuth(clientId: string, claimToken: string) {\n this.clientId = clientId;\n this.claimToken = claimToken;\n }\n\n queueEvent(event: TelemetryEvent) {\n this.events.push(event);\n }\n\n /**\n * Queue multiple pre-formed events (used by store-forward recovery).\n */\n queueEvents(events: TelemetryEvent[]): void {\n this.events.push(...events);\n }\n\n /**\n * Flush queued events. Returns true if events were sent or intentionally\n * dropped (4xx), false if they should be retried (5xx/network error).\n * Uses splice to only remove the events that were in the snapshot,\n * protecting any events queued concurrently during the fetch.\n */\n async flush(): Promise<boolean> {\n // Coalesce overlapping flushes: a second caller during an in-flight flush\n // would otherwise snapshot and POST the same events again (duplicate send),\n // and its splice() could drop events queued after the first flush started.\n if (this.flushInFlight) return this.flushInFlight;\n this.flushInFlight = this.flushInternal();\n try {\n return await this.flushInFlight;\n } finally {\n this.flushInFlight = null;\n }\n }\n\n private async flushInternal(): Promise<boolean> {\n if (this.events.length === 0) return true;\n if (!this.gatewayUrl) {\n debug('[Telemetry] No telemetry URL configured, skipping flush');\n return false;\n }\n\n const count = this.events.length;\n const payload: TelemetryRequest = { events: this.events.slice(0, count) };\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n // Read fresh credentials to handle token refresh mid-session. Skip an\n // expired stored token — sending a dead Bearer 401s and the event is\n // dropped, so fall through to claim-token / api-key auth instead.\n const freshCreds = getCredentials();\n const token = freshCreds?.accessToken\n ? isTokenExpired(freshCreds)\n ? null\n : freshCreds.accessToken\n : this.accessToken;\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n } else if (this.claimToken && this.clientId) {\n // Unclaimed environment auth path — guard accepts this instead of JWT\n headers['x-workos-claim-token'] = this.claimToken;\n headers['x-workos-client-id'] = this.clientId;\n } else if (this.apiKey) {\n headers['x-workos-api-key'] = this.apiKey;\n }\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000);\n\n try {\n if (isDebugEnabled()) {\n const eventSummary = payload.events.map(summarizeEvent).join('\\n ');\n debug(\n `[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry:\\n ${eventSummary}`,\n );\n }\n\n const response = await fetch(`${this.gatewayUrl}/telemetry`, {\n method: 'POST',\n headers,\n body: JSON.stringify(payload),\n signal: controller.signal,\n });\n\n if (response.ok) {\n this.events.splice(0, count);\n return true;\n } else {\n debug(`[Telemetry] Failed to send: ${response.status}`);\n // Drop on 4xx (permanent failures like 401/403 won't succeed on retry).\n // Retain on 5xx (transient server errors) for store-forward.\n if (response.status >= 400 && response.status < 500) {\n this.events.splice(0, count);\n return true; // intentionally dropped\n }\n return false;\n }\n } catch (error) {\n debug(`[Telemetry] Error sending events: ${error}`);\n // Events remain in queue for store-forward to persist\n return false;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n /**\n * Synchronously write pending events to a file.\n * Used as last resort in process.on('exit') handler.\n */\n persistToFile(filePath: string): void {\n if (this.events.length === 0) return;\n try {\n // Restrictive modes — the payload carries device/user identifiers.\n mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });\n writeFileSync(filePath, JSON.stringify(this.events), { encoding: 'utf-8', mode: 0o600 });\n this.events = [];\n } catch {\n // Silent failure — telemetry must never block exit\n }\n }\n}\n\nexport const telemetryClient = new TelemetryClient();\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Register a sync exit handler that persists unsent events to disk.
3
+ * Called once at startup. Uses PID in filename to prevent concurrent
4
+ * CLI invocations from colliding.
5
+ */
6
+ export declare function installStoreForward(): void;
7
+ /**
8
+ * On startup, check for ANY pending files from previous invocations
9
+ * (could be from different PIDs) and send them. Non-blocking, fire-and-forget.
10
+ */
11
+ export declare function recoverPendingEvents(): Promise<void>;
@@ -0,0 +1,94 @@
1
+ import { readFileSync, readdirSync, statSync, unlinkSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { telemetryClient } from './telemetry-client.js';
5
+ import { debug } from './debug.js';
6
+ const PENDING_DIR = join(tmpdir(), 'workos-cli-telemetry');
7
+ const PENDING_FILE = join(PENDING_DIR, `pending-${process.pid}.json`);
8
+ const MAX_PENDING_FILES = 100;
9
+ const MAX_PENDING_AGE_MS = 7 * 24 * 60 * 60 * 1000;
10
+ function safeUnlink(filePath) {
11
+ try {
12
+ unlinkSync(filePath);
13
+ }
14
+ catch {
15
+ /* ignore */
16
+ }
17
+ }
18
+ /**
19
+ * Register a sync exit handler that persists unsent events to disk.
20
+ * Called once at startup. Uses PID in filename to prevent concurrent
21
+ * CLI invocations from colliding.
22
+ */
23
+ export function installStoreForward() {
24
+ process.on('exit', () => {
25
+ telemetryClient.persistToFile(PENDING_FILE);
26
+ });
27
+ }
28
+ /**
29
+ * On startup, check for ANY pending files from previous invocations
30
+ * (could be from different PIDs) and send them. Non-blocking, fire-and-forget.
31
+ */
32
+ export async function recoverPendingEvents() {
33
+ try {
34
+ if (!existsSync(PENDING_DIR))
35
+ return;
36
+ const files = readdirSync(PENDING_DIR).filter((f) => f.startsWith('pending-') && f.endsWith('.json'));
37
+ const now = Date.now();
38
+ const pendingFiles = [];
39
+ for (const file of files) {
40
+ const filePath = join(PENDING_DIR, file);
41
+ try {
42
+ const { mtimeMs } = statSync(filePath);
43
+ if (now - mtimeMs > MAX_PENDING_AGE_MS) {
44
+ debug(`[Telemetry] Dropping stale pending file: ${file}`);
45
+ safeUnlink(filePath);
46
+ }
47
+ else {
48
+ pendingFiles.push({ file, filePath, mtimeMs });
49
+ }
50
+ }
51
+ catch {
52
+ debug(`[Telemetry] Dropping unreadable pending file: ${file}`);
53
+ safeUnlink(filePath);
54
+ }
55
+ }
56
+ pendingFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
57
+ const filesToRecover = pendingFiles.slice(0, MAX_PENDING_FILES);
58
+ for (const dropped of pendingFiles.slice(MAX_PENDING_FILES)) {
59
+ debug(`[Telemetry] Dropping excess pending file: ${dropped.file}`);
60
+ safeUnlink(dropped.filePath);
61
+ }
62
+ const recoveredFiles = [];
63
+ for (const { filePath } of filesToRecover) {
64
+ try {
65
+ const raw = readFileSync(filePath, 'utf-8');
66
+ const events = JSON.parse(raw);
67
+ if (Array.isArray(events) && events.length > 0) {
68
+ telemetryClient.queueEvents(events);
69
+ recoveredFiles.push(filePath);
70
+ }
71
+ else {
72
+ // Empty file — delete immediately
73
+ safeUnlink(filePath);
74
+ }
75
+ }
76
+ catch {
77
+ // Corrupted file — delete and move on
78
+ safeUnlink(filePath);
79
+ }
80
+ }
81
+ // Delete source files — events are now in memory regardless of flush outcome.
82
+ // If flush succeeds: events sent, done.
83
+ // If flush fails: events stay in memory, exit handler re-persists to new PID file.
84
+ for (const filePath of recoveredFiles) {
85
+ safeUnlink(filePath);
86
+ }
87
+ // Flush all recovered events in one batch
88
+ await telemetryClient.flush();
89
+ }
90
+ catch {
91
+ debug('[Telemetry] Store-forward recovery failed silently');
92
+ }
93
+ }
94
+ //# sourceMappingURL=telemetry-store-forward.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry-store-forward.js","sourceRoot":"","sources":["../../src/utils/telemetry-store-forward.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync, readdirSync, statSync, unlinkSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { telemetryClient } from './telemetry-client.js';\nimport { debug } from './debug.js';\n\nconst PENDING_DIR = join(tmpdir(), 'workos-cli-telemetry');\nconst PENDING_FILE = join(PENDING_DIR, `pending-${process.pid}.json`);\nconst MAX_PENDING_FILES = 100;\nconst MAX_PENDING_AGE_MS = 7 * 24 * 60 * 60 * 1000;\n\nfunction safeUnlink(filePath: string): void {\n try {\n unlinkSync(filePath);\n } catch {\n /* ignore */\n }\n}\n\n/**\n * Register a sync exit handler that persists unsent events to disk.\n * Called once at startup. Uses PID in filename to prevent concurrent\n * CLI invocations from colliding.\n */\nexport function installStoreForward(): void {\n process.on('exit', () => {\n telemetryClient.persistToFile(PENDING_FILE);\n });\n}\n\n/**\n * On startup, check for ANY pending files from previous invocations\n * (could be from different PIDs) and send them. Non-blocking, fire-and-forget.\n */\nexport async function recoverPendingEvents(): Promise<void> {\n try {\n if (!existsSync(PENDING_DIR)) return;\n const files = readdirSync(PENDING_DIR).filter((f) => f.startsWith('pending-') && f.endsWith('.json'));\n const now = Date.now();\n const pendingFiles: Array<{ file: string; filePath: string; mtimeMs: number }> = [];\n\n for (const file of files) {\n const filePath = join(PENDING_DIR, file);\n try {\n const { mtimeMs } = statSync(filePath);\n if (now - mtimeMs > MAX_PENDING_AGE_MS) {\n debug(`[Telemetry] Dropping stale pending file: ${file}`);\n safeUnlink(filePath);\n } else {\n pendingFiles.push({ file, filePath, mtimeMs });\n }\n } catch {\n debug(`[Telemetry] Dropping unreadable pending file: ${file}`);\n safeUnlink(filePath);\n }\n }\n\n pendingFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);\n const filesToRecover = pendingFiles.slice(0, MAX_PENDING_FILES);\n for (const dropped of pendingFiles.slice(MAX_PENDING_FILES)) {\n debug(`[Telemetry] Dropping excess pending file: ${dropped.file}`);\n safeUnlink(dropped.filePath);\n }\n\n const recoveredFiles: string[] = [];\n for (const { filePath } of filesToRecover) {\n try {\n const raw = readFileSync(filePath, 'utf-8');\n const events = JSON.parse(raw);\n if (Array.isArray(events) && events.length > 0) {\n telemetryClient.queueEvents(events);\n recoveredFiles.push(filePath);\n } else {\n // Empty file — delete immediately\n safeUnlink(filePath);\n }\n } catch {\n // Corrupted file — delete and move on\n safeUnlink(filePath);\n }\n }\n\n // Delete source files — events are now in memory regardless of flush outcome.\n // If flush succeeds: events sent, done.\n // If flush fails: events stay in memory, exit handler re-persists to new PID file.\n for (const filePath of recoveredFiles) {\n safeUnlink(filePath);\n }\n\n // Flush all recovered events in one batch\n await telemetryClient.flush();\n } catch {\n debug('[Telemetry] Store-forward recovery failed silently');\n }\n}\n"]}
@@ -2,31 +2,49 @@
2
2
  * Telemetry event types for installer → gateway communication.
3
3
  * The gateway converts these to OTel format.
4
4
  */
5
- export interface TelemetryEvent {
6
- type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm';
5
+ export interface BaseTelemetryEvent {
6
+ type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm' | 'command' | 'crash';
7
7
  sessionId: string;
8
8
  timestamp: string;
9
- attributes?: Record<string, string | number | boolean>;
10
9
  }
11
- export interface SessionStartEvent extends TelemetryEvent {
10
+ export type AuthMode = 'jwt' | 'claim_token' | 'api_key' | 'none';
11
+ /**
12
+ * Structured outcome dimension for command events. Supersedes the boolean
13
+ * `command.success` as the primary categorization (`command.success` remains
14
+ * for backward-compat). Populated by `analytics.emitCommandEvent()` from the
15
+ * top-level command lifecycle.
16
+ */
17
+ export type TerminationReason = 'success' | 'cancelled' | 'auth_required' | 'validation_error' | 'api_error' | 'crash';
18
+ export interface EnvFingerprint {
19
+ 'device.id': string;
20
+ 'auth.mode': AuthMode;
21
+ 'env.os': string;
22
+ 'env.os_version': string;
23
+ 'env.node_version': string;
24
+ 'env.shell': string;
25
+ 'env.ci': boolean;
26
+ 'env.ci_provider'?: string;
27
+ }
28
+ export interface SessionStartEvent extends BaseTelemetryEvent {
12
29
  type: 'session.start';
13
30
  attributes: {
14
31
  'installer.version': string;
15
32
  'installer.mode': 'cli' | 'tui' | 'headless';
16
33
  'workos.user_id'?: string;
17
34
  'workos.org_id'?: string;
18
- };
35
+ } & EnvFingerprint;
19
36
  }
20
- export interface SessionEndEvent extends TelemetryEvent {
37
+ export interface SessionEndEvent extends BaseTelemetryEvent {
21
38
  type: 'session.end';
22
39
  attributes: {
23
40
  'installer.outcome': 'success' | 'error' | 'cancelled';
24
41
  'installer.duration_ms': number;
25
42
  } & Record<string, string | number | boolean>;
26
43
  }
27
- export interface StepEvent extends TelemetryEvent {
44
+ export interface StepEvent extends BaseTelemetryEvent {
28
45
  type: 'step';
29
46
  name: string;
47
+ startTimestamp: string;
30
48
  durationMs: number;
31
49
  success: boolean;
32
50
  error?: {
@@ -34,18 +52,49 @@ export interface StepEvent extends TelemetryEvent {
34
52
  message: string;
35
53
  };
36
54
  }
37
- export interface AgentToolEvent extends TelemetryEvent {
55
+ export interface AgentToolEvent extends BaseTelemetryEvent {
38
56
  type: 'agent.tool';
39
57
  toolName: string;
58
+ startTimestamp: string;
40
59
  durationMs: number;
41
60
  success: boolean;
42
61
  }
43
- export interface AgentLLMEvent extends TelemetryEvent {
62
+ export interface AgentLLMEvent extends BaseTelemetryEvent {
44
63
  type: 'agent.llm';
45
64
  model: string;
46
65
  inputTokens: number;
47
66
  outputTokens: number;
48
67
  }
68
+ export interface CommandEvent extends BaseTelemetryEvent {
69
+ type: 'command';
70
+ attributes: {
71
+ 'command.name': string;
72
+ 'command.duration_ms': number;
73
+ 'command.success': boolean;
74
+ 'command.error_type'?: string;
75
+ 'command.error_message'?: string;
76
+ 'command.flags'?: string;
77
+ 'termination.reason'?: TerminationReason;
78
+ 'error.code'?: string;
79
+ 'api.status'?: number;
80
+ 'api.code'?: string;
81
+ 'api.resource'?: string;
82
+ 'cli.version': string;
83
+ 'workos.user_id'?: string;
84
+ } & EnvFingerprint;
85
+ }
86
+ export interface CrashEvent extends BaseTelemetryEvent {
87
+ type: 'crash';
88
+ attributes: {
89
+ 'crash.error_type': string;
90
+ 'crash.error_message': string;
91
+ 'crash.stack': string;
92
+ 'crash.command'?: string;
93
+ 'cli.version': string;
94
+ 'workos.user_id'?: string;
95
+ } & EnvFingerprint;
96
+ }
49
97
  export interface TelemetryRequest {
50
98
  events: TelemetryEvent[];
51
99
  }
100
+ export type TelemetryEvent = SessionStartEvent | SessionEndEvent | StepEvent | AgentToolEvent | AgentLLMEvent | CommandEvent | CrashEvent;
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry-types.js","sourceRoot":"","sources":["../../src/utils/telemetry-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * Telemetry event types for installer → gateway communication.\n * The gateway converts these to OTel format.\n */\n\nexport interface TelemetryEvent {\n type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm';\n sessionId: string;\n timestamp: string;\n attributes?: Record<string, string | number | boolean>;\n}\n\nexport interface SessionStartEvent extends TelemetryEvent {\n type: 'session.start';\n attributes: {\n 'installer.version': string;\n 'installer.mode': 'cli' | 'tui' | 'headless';\n 'workos.user_id'?: string;\n 'workos.org_id'?: string;\n };\n}\n\nexport interface SessionEndEvent extends TelemetryEvent {\n type: 'session.end';\n attributes: {\n 'installer.outcome': 'success' | 'error' | 'cancelled';\n 'installer.duration_ms': number;\n } & Record<string, string | number | boolean>;\n}\n\nexport interface StepEvent extends TelemetryEvent {\n type: 'step';\n name: string;\n durationMs: number;\n success: boolean;\n error?: {\n type: string;\n message: string;\n };\n}\n\nexport interface AgentToolEvent extends TelemetryEvent {\n type: 'agent.tool';\n toolName: string;\n durationMs: number;\n success: boolean;\n}\n\nexport interface AgentLLMEvent extends TelemetryEvent {\n type: 'agent.llm';\n model: string;\n inputTokens: number;\n outputTokens: number;\n}\n\nexport interface TelemetryRequest {\n events: TelemetryEvent[];\n}\n"]}
1
+ {"version":3,"file":"telemetry-types.js","sourceRoot":"","sources":["../../src/utils/telemetry-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * Telemetry event types for installer → gateway communication.\n * The gateway converts these to OTel format.\n */\n\nexport interface BaseTelemetryEvent {\n type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm' | 'command' | 'crash';\n sessionId: string;\n timestamp: string;\n}\n\nexport type AuthMode = 'jwt' | 'claim_token' | 'api_key' | 'none';\n\n/**\n * Structured outcome dimension for command events. Supersedes the boolean\n * `command.success` as the primary categorization (`command.success` remains\n * for backward-compat). Populated by `analytics.emitCommandEvent()` from the\n * top-level command lifecycle.\n */\nexport type TerminationReason = 'success' | 'cancelled' | 'auth_required' | 'validation_error' | 'api_error' | 'crash';\n\nexport interface EnvFingerprint {\n 'device.id': string;\n 'auth.mode': AuthMode;\n 'env.os': string;\n 'env.os_version': string;\n 'env.node_version': string;\n 'env.shell': string;\n 'env.ci': boolean;\n 'env.ci_provider'?: string;\n}\n\nexport interface SessionStartEvent extends BaseTelemetryEvent {\n type: 'session.start';\n attributes: {\n 'installer.version': string;\n 'installer.mode': 'cli' | 'tui' | 'headless';\n 'workos.user_id'?: string;\n 'workos.org_id'?: string;\n } & EnvFingerprint;\n}\n\nexport interface SessionEndEvent extends BaseTelemetryEvent {\n type: 'session.end';\n attributes: {\n 'installer.outcome': 'success' | 'error' | 'cancelled';\n 'installer.duration_ms': number;\n } & Record<string, string | number | boolean>;\n}\n\nexport interface StepEvent extends BaseTelemetryEvent {\n type: 'step';\n name: string;\n startTimestamp: string;\n durationMs: number;\n success: boolean;\n error?: {\n type: string;\n message: string;\n };\n}\n\nexport interface AgentToolEvent extends BaseTelemetryEvent {\n type: 'agent.tool';\n toolName: string;\n startTimestamp: string;\n durationMs: number;\n success: boolean;\n}\n\nexport interface AgentLLMEvent extends BaseTelemetryEvent {\n type: 'agent.llm';\n model: string;\n inputTokens: number;\n outputTokens: number;\n}\n\nexport interface CommandEvent extends BaseTelemetryEvent {\n type: 'command';\n attributes: {\n 'command.name': string;\n 'command.duration_ms': number;\n 'command.success': boolean;\n 'command.error_type'?: string;\n 'command.error_message'?: string;\n 'command.flags'?: string;\n 'termination.reason'?: TerminationReason;\n 'error.code'?: string;\n 'api.status'?: number;\n 'api.code'?: string;\n 'api.resource'?: string;\n 'cli.version': string;\n 'workos.user_id'?: string;\n } & EnvFingerprint;\n}\n\nexport interface CrashEvent extends BaseTelemetryEvent {\n type: 'crash';\n attributes: {\n 'crash.error_type': string;\n 'crash.error_message': string;\n 'crash.stack': string;\n 'crash.command'?: string;\n 'cli.version': string;\n 'workos.user_id'?: string;\n } & EnvFingerprint;\n}\n\nexport interface TelemetryRequest {\n events: TelemetryEvent[];\n}\n\nexport type TelemetryEvent =\n | SessionStartEvent\n | SessionEndEvent\n | StepEvent\n | AgentToolEvent\n | AgentLLMEvent\n | CommandEvent\n | CrashEvent;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workos",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "description": "The Official Workos CLI",
6
6
  "repository": {