@irogane/kaji 0.2.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +105 -0
- package/README.md +598 -0
- package/contracts/README.md +14 -0
- package/contracts/beta-core-v1.json +52 -0
- package/contracts/cli/init-cases-v1.json +27 -0
- package/contracts/errors/error-codes.json +48 -0
- package/contracts/errors/integration-recovery-v1.json +127 -0
- package/contracts/errors/provider-normalization.json +111 -0
- package/contracts/events/conformance-invalid.json +20 -0
- package/contracts/events/conformance.json +511 -0
- package/contracts/events/new-kaji-event-v1.schema.json +1021 -0
- package/contracts/events/stored-kaji-event-v1.schema.json +1025 -0
- package/contracts/feature-tiers-v1.json +488 -0
- package/contracts/integrations/abi-index-v1.json +8 -0
- package/contracts/integrations/conformance-invalid.json +443 -0
- package/contracts/integrations/conformance-valid.json +121 -0
- package/contracts/integrations/copy-provenance-v1.schema.json +61 -0
- package/contracts/integrations/echo-tool-abi-v1.json +37 -0
- package/contracts/integrations/github-api-conformance-v1.json +644 -0
- package/contracts/integrations/github-tool-abi-typescript-v1.json +369 -0
- package/contracts/integrations/github-tool-abi-v1.json +146 -0
- package/contracts/integrations/gmail-api-conformance-v1.json +750 -0
- package/contracts/integrations/gmail-tool-abi-v1.json +62 -0
- package/contracts/integrations/index.schema.json +37 -0
- package/contracts/integrations/manifest.schema.json +119 -0
- package/contracts/parity/expected-normalized.json +4906 -0
- package/contracts/parity/scenarios.json +100 -0
- package/contracts/parity/scenarios.schema.json +214 -0
- package/contracts/providers/cost-conformance.json +112 -0
- package/contracts/release/github-proof-v1.schema.json +138 -0
- package/contracts/release/gmail-proof-v1.schema.json +138 -0
- package/contracts/release/kaji-ts-consumer-handoff-v1.schema.json +1289 -0
- package/contracts/release/publisher-identity-receipt-v1.schema.json +319 -0
- package/contracts/release/typescript-onboarding-evidence-v1.schema.json +740 -0
- package/contracts/tools/conformance-invalid.json +271 -0
- package/contracts/tools/conformance-valid.json +78 -0
- package/contracts/tools/tool-schema-v1.schema.json +18 -0
- package/dist/anthropic.cjs +1231 -0
- package/dist/anthropic.cjs.map +1 -0
- package/dist/anthropic.d.cts +26 -0
- package/dist/anthropic.d.ts +26 -0
- package/dist/anthropic.js +270 -0
- package/dist/anthropic.js.map +1 -0
- package/dist/auth.cjs +1507 -0
- package/dist/auth.cjs.map +1 -0
- package/dist/auth.d.cts +129 -0
- package/dist/auth.d.ts +129 -0
- package/dist/auth.js +1039 -0
- package/dist/auth.js.map +1 -0
- package/dist/base-B9FRMcP8.d.cts +140 -0
- package/dist/base-nHQd1VtS.d.ts +140 -0
- package/dist/chunk-AAM33KAO.js +4367 -0
- package/dist/chunk-AAM33KAO.js.map +1 -0
- package/dist/chunk-KAJ6BM64.js +153 -0
- package/dist/chunk-KAJ6BM64.js.map +1 -0
- package/dist/chunk-KCAXIOZS.js +308 -0
- package/dist/chunk-KCAXIOZS.js.map +1 -0
- package/dist/chunk-LSJ4AVO2.js +243 -0
- package/dist/chunk-LSJ4AVO2.js.map +1 -0
- package/dist/chunk-TM7ZGOJX.js +716 -0
- package/dist/chunk-TM7ZGOJX.js.map +1 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +13 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/chunk-2RCWPRVY.js +6277 -0
- package/dist/cli/chunk-2RCWPRVY.js.map +1 -0
- package/dist/cli/chunk-SEBX54TR.js +681 -0
- package/dist/cli/chunk-SEBX54TR.js.map +1 -0
- package/dist/cli/index.d.ts +232 -0
- package/dist/cli/index.js +11 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-worker.d.ts +2 -0
- package/dist/cli/init-worker.js +18 -0
- package/dist/cli/init-worker.js.map +1 -0
- package/dist/cli/integration-copy-worker.js +48 -0
- package/dist/cli/integration-copy-worker.js.map +1 -0
- package/dist/cli/package-entry-cjs.cjs +21 -0
- package/dist/cli/package-entry-cjs.cjs.map +1 -0
- package/dist/cli/package-entry-cjs.d.cts +2 -0
- package/dist/cli/package-entry.d.ts +2 -0
- package/dist/cli/package-entry.js +13 -0
- package/dist/cli/package-entry.js.map +1 -0
- package/dist/context-BaFHrQHv.d.cts +21 -0
- package/dist/context-BaFHrQHv.d.ts +21 -0
- package/dist/context-C-YPY-GS.d.cts +1538 -0
- package/dist/context-C-YPY-GS.d.ts +1538 -0
- package/dist/index.cjs +12346 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1560 -0
- package/dist/index.d.ts +1560 -0
- package/dist/index.js +7852 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/github.cjs +2092 -0
- package/dist/integrations/github.cjs.map +1 -0
- package/dist/integrations/github.d.cts +21 -0
- package/dist/integrations/github.d.ts +21 -0
- package/dist/integrations/github.js +2088 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations.cjs +3370 -0
- package/dist/integrations.cjs.map +1 -0
- package/dist/integrations.d.cts +202 -0
- package/dist/integrations.d.ts +202 -0
- package/dist/integrations.js +2650 -0
- package/dist/integrations.js.map +1 -0
- package/dist/observability-Cj--OkME.d.cts +96 -0
- package/dist/observability-Cj--OkME.d.ts +96 -0
- package/dist/openai.cjs +1235 -0
- package/dist/openai.cjs.map +1 -0
- package/dist/openai.d.cts +32 -0
- package/dist/openai.d.ts +32 -0
- package/dist/openai.js +272 -0
- package/dist/openai.js.map +1 -0
- package/dist/testing.cjs +554 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +43 -0
- package/dist/testing.d.ts +43 -0
- package/dist/testing.js +118 -0
- package/dist/testing.js.map +1 -0
- package/package.json +188 -0
- package/registry/echo/index.ts +53 -0
- package/registry/echo/manifest.json +53 -0
- package/registry/github/LICENSE +105 -0
- package/registry/github/client.ts +1727 -0
- package/registry/github/index.ts +263 -0
- package/registry/github/manifest.json +227 -0
- package/registry/github/owner-fixtures.json +10 -0
- package/registry/github/tests/github.test.ts +32 -0
- package/registry/gmail/LICENSE +105 -0
- package/registry/gmail/client.ts +548 -0
- package/registry/gmail/index.ts +165 -0
- package/registry/gmail/manifest.json +102 -0
- package/registry/gmail/owner-fixtures.json +10 -0
- package/registry/gmail/tests/gmail.test.ts +32 -0
- package/registry/index.json +21 -0
- package/registry/index.schema.json +37 -0
- package/registry/schema.json +119 -0
package/dist/auth.cjs
ADDED
|
@@ -0,0 +1,1507 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var http = require('http');
|
|
5
|
+
var https = require('https');
|
|
6
|
+
var child_process = require('child_process');
|
|
7
|
+
var fs = require('fs');
|
|
8
|
+
var os = require('os');
|
|
9
|
+
|
|
10
|
+
// src/auth/oauth.ts
|
|
11
|
+
|
|
12
|
+
// src/contracts/integration-recovery.ts
|
|
13
|
+
var DOCS = "https://kaji.dev/docs/integrations/recovery-v1";
|
|
14
|
+
function recovery(errorCode, recoveryCode, anchor, problem, cause, fix) {
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
errorCode,
|
|
17
|
+
recoveryCode,
|
|
18
|
+
docUrl: `${DOCS}#${anchor}`,
|
|
19
|
+
problem,
|
|
20
|
+
cause,
|
|
21
|
+
fix
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
var INTEGRATION_RECOVERY = Object.freeze({
|
|
25
|
+
github_token_missing: recovery(
|
|
26
|
+
"INTEGRATION_AUTH_REQUIRED",
|
|
27
|
+
"CONFIGURE_GITHUB_TOKEN",
|
|
28
|
+
"github-token",
|
|
29
|
+
"GitHub authentication is required.",
|
|
30
|
+
"The configured GitHub token is unavailable.",
|
|
31
|
+
"Set the manifest-declared GitHub token environment variable and retry."
|
|
32
|
+
),
|
|
33
|
+
gmail_grant_missing: recovery(
|
|
34
|
+
"INTEGRATION_AUTH_REQUIRED",
|
|
35
|
+
"CONNECT_GMAIL",
|
|
36
|
+
"gmail-grant",
|
|
37
|
+
"Gmail authorization is required.",
|
|
38
|
+
"No Gmail grant exists for this principal.",
|
|
39
|
+
"Use the package-qualified connect command for this runtime and principal, then retry."
|
|
40
|
+
),
|
|
41
|
+
keychain_missing: recovery(
|
|
42
|
+
"INTEGRATION_AUTH_ERROR",
|
|
43
|
+
"RESTORE_KEYCHAIN",
|
|
44
|
+
"keychain-missing",
|
|
45
|
+
"The macOS Keychain is unavailable.",
|
|
46
|
+
"The Keychain command could not be found.",
|
|
47
|
+
"Restore `/usr/bin/security` on a supported macOS host and retry."
|
|
48
|
+
),
|
|
49
|
+
keychain_locked: recovery(
|
|
50
|
+
"INTEGRATION_AUTH_ERROR",
|
|
51
|
+
"UNLOCK_KEYCHAIN",
|
|
52
|
+
"keychain-locked",
|
|
53
|
+
"The macOS Keychain is locked.",
|
|
54
|
+
"The stored integration grant cannot be read while Keychain is locked.",
|
|
55
|
+
"Unlock the login Keychain and retry."
|
|
56
|
+
),
|
|
57
|
+
keychain_corrupt: recovery(
|
|
58
|
+
"INTEGRATION_AUTH_ERROR",
|
|
59
|
+
"RESET_GMAIL_GRANT",
|
|
60
|
+
"keychain-corrupt",
|
|
61
|
+
"The stored Gmail grant is invalid.",
|
|
62
|
+
"The Keychain entry is corrupt or incomplete.",
|
|
63
|
+
"Disconnect Gmail locally, then reconnect with the same principal."
|
|
64
|
+
),
|
|
65
|
+
keychain_unsupported: recovery(
|
|
66
|
+
"INTEGRATION_AUTH_ERROR",
|
|
67
|
+
"USE_SUPPORTED_KEYCHAIN",
|
|
68
|
+
"keychain-unsupported",
|
|
69
|
+
"Secure Gmail grant storage is unsupported.",
|
|
70
|
+
"This host cannot provide the required macOS Keychain boundary.",
|
|
71
|
+
"Use a supported macOS host; do not fall back to plaintext storage."
|
|
72
|
+
),
|
|
73
|
+
gmail_scope_drift: recovery(
|
|
74
|
+
"INTEGRATION_AUTH_REQUIRED",
|
|
75
|
+
"RECONNECT_GMAIL",
|
|
76
|
+
"gmail-scope-drift",
|
|
77
|
+
"The Gmail grant no longer has the required scopes.",
|
|
78
|
+
"Stored and required OAuth scopes differ.",
|
|
79
|
+
"Use the package-qualified connect command for this runtime and principal, then consent again."
|
|
80
|
+
),
|
|
81
|
+
policy_rejected: recovery(
|
|
82
|
+
"INTEGRATION_POLICY_REJECTED",
|
|
83
|
+
"REVIEW_INTEGRATION_POLICY",
|
|
84
|
+
"policy-rejected",
|
|
85
|
+
"The integration request was rejected before dispatch.",
|
|
86
|
+
"A fixed-origin, repository, recipient, or schema policy did not match.",
|
|
87
|
+
"Review the integration allowlist and request shape before retrying."
|
|
88
|
+
),
|
|
89
|
+
api_rejected: recovery(
|
|
90
|
+
"INTEGRATION_API_ERROR",
|
|
91
|
+
"REVIEW_PROVIDER_REJECTION",
|
|
92
|
+
"api-rejected",
|
|
93
|
+
"The provider rejected the integration request.",
|
|
94
|
+
"The provider returned a confirmed non-retryable API rejection.",
|
|
95
|
+
"Review the request against the provider response guidance before retrying manually."
|
|
96
|
+
),
|
|
97
|
+
transient_read_failed: recovery(
|
|
98
|
+
"INTEGRATION_API_ERROR",
|
|
99
|
+
"RETRY_INTEGRATION_READ",
|
|
100
|
+
"transient-read",
|
|
101
|
+
"The integration read did not complete.",
|
|
102
|
+
"The provider returned a transient failure before any external effect.",
|
|
103
|
+
"Retry the idempotent read with bounded backoff."
|
|
104
|
+
),
|
|
105
|
+
rate_limited: recovery(
|
|
106
|
+
"INTEGRATION_RATE_LIMITED",
|
|
107
|
+
"RETRY_AFTER_RATE_LIMIT",
|
|
108
|
+
"rate-limited",
|
|
109
|
+
"The provider rate limit was reached.",
|
|
110
|
+
"The provider confirmed that this request was rate limited.",
|
|
111
|
+
"Reduce parallelism and retry only after the bounded provider delay."
|
|
112
|
+
),
|
|
113
|
+
redirect_rejected: recovery(
|
|
114
|
+
"INTEGRATION_REDIRECT_REJECTED",
|
|
115
|
+
"REJECT_PROVIDER_REDIRECT",
|
|
116
|
+
"redirect-rejected",
|
|
117
|
+
"The provider returned a redirect that Kaji will not follow.",
|
|
118
|
+
"Fixed-origin integration requests reject every redirect.",
|
|
119
|
+
"Verify the provider endpoint; do not retry an ambiguous mutation blindly."
|
|
120
|
+
),
|
|
121
|
+
response_limit_exceeded: recovery(
|
|
122
|
+
"INTEGRATION_RESPONSE_LIMIT",
|
|
123
|
+
"REDUCE_INTEGRATION_RESPONSE",
|
|
124
|
+
"response-limit",
|
|
125
|
+
"The integration response exceeded a safety bound.",
|
|
126
|
+
"Provider headers or body were malformed or too large.",
|
|
127
|
+
"Narrow the read request; reconcile a mutation before any retry."
|
|
128
|
+
),
|
|
129
|
+
github_mutation_unknown: recovery(
|
|
130
|
+
"TOOL_EXECUTION_FAILED",
|
|
131
|
+
"RECONCILE_GITHUB_MUTATION",
|
|
132
|
+
"github-mutation-unknown",
|
|
133
|
+
"The GitHub mutation outcome is unknown.",
|
|
134
|
+
"The connection failed after dispatch without a confirmed provider result.",
|
|
135
|
+
"Reconcile the GitHub issue or comment marker before any manual retry."
|
|
136
|
+
),
|
|
137
|
+
gmail_mutation_unknown: recovery(
|
|
138
|
+
"TOOL_EXECUTION_FAILED",
|
|
139
|
+
"RECONCILE_GMAIL_MUTATION",
|
|
140
|
+
"gmail-mutation-unknown",
|
|
141
|
+
"The Gmail mutation outcome is unknown.",
|
|
142
|
+
"The connection failed after dispatch without a confirmed provider result.",
|
|
143
|
+
"Reconcile the Gmail draft key or sent message before any manual retry."
|
|
144
|
+
)
|
|
145
|
+
});
|
|
146
|
+
function recoveryForReason(reasonCode) {
|
|
147
|
+
const value = INTEGRATION_RECOVERY[reasonCode];
|
|
148
|
+
if (value === void 0) throw new TypeError("unknown integration recovery reason");
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/tools/execution-errors.ts
|
|
153
|
+
var TOOL_EXECUTION_ERROR_BRAND = /* @__PURE__ */ Symbol.for("@kaji/sdk.ToolExecutionError.v1");
|
|
154
|
+
function nativeInstanceOf(constructor, value) {
|
|
155
|
+
return Function.prototype[Symbol.hasInstance].call(constructor, value);
|
|
156
|
+
}
|
|
157
|
+
var ToolExecutionError = class _ToolExecutionError extends Error {
|
|
158
|
+
constructor(message, error_code, retryable, outcome, recovery2) {
|
|
159
|
+
super(message);
|
|
160
|
+
this.error_code = error_code;
|
|
161
|
+
this.retryable = retryable;
|
|
162
|
+
this.outcome = outcome;
|
|
163
|
+
this.name = "ToolExecutionError";
|
|
164
|
+
Object.defineProperty(this, TOOL_EXECUTION_ERROR_BRAND, { value: true });
|
|
165
|
+
if (recovery2 !== void 0) {
|
|
166
|
+
this.reason_code = recovery2.reason_code;
|
|
167
|
+
this.recovery_code = recovery2.recovery_code;
|
|
168
|
+
this.doc_url = recovery2.doc_url;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
error_code;
|
|
172
|
+
retryable;
|
|
173
|
+
outcome;
|
|
174
|
+
static [Symbol.hasInstance](value) {
|
|
175
|
+
if (this !== _ToolExecutionError) return nativeInstanceOf(this, value);
|
|
176
|
+
return nativeInstanceOf(this, value) || value instanceof Error && Reflect.get(value, TOOL_EXECUTION_ERROR_BRAND) === true;
|
|
177
|
+
}
|
|
178
|
+
reason_code;
|
|
179
|
+
recovery_code;
|
|
180
|
+
doc_url;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/integrations/errors.ts
|
|
184
|
+
function certified(kind, reasonCode, retryable) {
|
|
185
|
+
return Object.freeze({ kind, recovery: recoveryForReason(reasonCode), retryable });
|
|
186
|
+
}
|
|
187
|
+
var CERTIFIED_FAILURES = Object.freeze({
|
|
188
|
+
api_rejected: certified("api", "api_rejected", false),
|
|
189
|
+
github_token_missing: certified("auth_required", "github_token_missing", false),
|
|
190
|
+
gmail_grant_missing: certified("auth_required", "gmail_grant_missing", false),
|
|
191
|
+
gmail_scope_drift: certified("auth_required", "gmail_scope_drift", false),
|
|
192
|
+
keychain_missing: certified("auth", "keychain_missing", false),
|
|
193
|
+
keychain_locked: certified("auth", "keychain_locked", false),
|
|
194
|
+
keychain_corrupt: certified("auth", "keychain_corrupt", false),
|
|
195
|
+
keychain_unsupported: certified("auth", "keychain_unsupported", false),
|
|
196
|
+
rate_limited: certified("rate_limited", "rate_limited", true),
|
|
197
|
+
transient_read_failed: certified("transient_read", "transient_read_failed", true),
|
|
198
|
+
policy_rejected: certified("policy", "policy_rejected", false)
|
|
199
|
+
});
|
|
200
|
+
function certifiedFailure(reasonCode) {
|
|
201
|
+
const failure = CERTIFIED_FAILURES[reasonCode];
|
|
202
|
+
if (failure === void 0) throw new TypeError("uncertified integration failure reason");
|
|
203
|
+
return failure;
|
|
204
|
+
}
|
|
205
|
+
function constructorKind(constructor) {
|
|
206
|
+
if (constructor === IntegrationExecutionError) return "api";
|
|
207
|
+
if (constructor === IntegrationAuthRequiredError) return "auth_required";
|
|
208
|
+
if (constructor === IntegrationAuthError) return "auth";
|
|
209
|
+
if (constructor === IntegrationRateLimitedError) return "rate_limited";
|
|
210
|
+
if (constructor === IntegrationTransientReadError) return "transient_read";
|
|
211
|
+
if (constructor === IntegrationPolicyError) return "policy";
|
|
212
|
+
return void 0;
|
|
213
|
+
}
|
|
214
|
+
var IntegrationExecutionError = class extends ToolExecutionError {
|
|
215
|
+
constructor(reasonCode) {
|
|
216
|
+
const failure = certifiedFailure(reasonCode);
|
|
217
|
+
if (failure.kind !== constructorKind(new.target)) {
|
|
218
|
+
throw new TypeError("integration failure reason is not certified for this error class");
|
|
219
|
+
}
|
|
220
|
+
const { recovery: recovery2 } = failure;
|
|
221
|
+
super("Tool execution failed", recovery2.errorCode, failure.retryable, "failed", {
|
|
222
|
+
reason_code: reasonCode,
|
|
223
|
+
recovery_code: recovery2.recoveryCode,
|
|
224
|
+
doc_url: recovery2.docUrl
|
|
225
|
+
});
|
|
226
|
+
this.name = "IntegrationExecutionError";
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var IntegrationAuthRequiredError = class extends IntegrationExecutionError {
|
|
230
|
+
constructor(reasonCode) {
|
|
231
|
+
super(reasonCode);
|
|
232
|
+
this.name = "IntegrationAuthRequiredError";
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
var IntegrationAuthError = class extends IntegrationExecutionError {
|
|
236
|
+
constructor(reasonCode) {
|
|
237
|
+
super(reasonCode);
|
|
238
|
+
this.name = "IntegrationAuthError";
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var IntegrationRateLimitedError = class extends IntegrationExecutionError {
|
|
242
|
+
constructor() {
|
|
243
|
+
super("rate_limited");
|
|
244
|
+
this.name = "IntegrationRateLimitedError";
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
var IntegrationTransientReadError = class extends IntegrationExecutionError {
|
|
248
|
+
constructor() {
|
|
249
|
+
super("transient_read_failed");
|
|
250
|
+
this.name = "IntegrationTransientReadError";
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
var IntegrationPolicyError = class extends IntegrationExecutionError {
|
|
254
|
+
constructor() {
|
|
255
|
+
super("policy_rejected");
|
|
256
|
+
this.name = "IntegrationPolicyError";
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var systemClock = Object.freeze({
|
|
260
|
+
nowWallSeconds: () => Date.now() / 1e3,
|
|
261
|
+
nowMonotonic: () => globalThis.performance.now()
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// src/observability.ts
|
|
265
|
+
var SPAN_NAMES = /* @__PURE__ */ new Set([
|
|
266
|
+
"kaji.turn",
|
|
267
|
+
"kaji.provider",
|
|
268
|
+
"kaji.tool",
|
|
269
|
+
"kaji.integration.auth",
|
|
270
|
+
"kaji.integration.request"
|
|
271
|
+
]);
|
|
272
|
+
var TRACE_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
273
|
+
"session.id",
|
|
274
|
+
"turn.id",
|
|
275
|
+
"request.id",
|
|
276
|
+
"trace.id",
|
|
277
|
+
"tool.call_id",
|
|
278
|
+
"provider.family",
|
|
279
|
+
"integration.name",
|
|
280
|
+
"integration.operation",
|
|
281
|
+
"http.status_family"
|
|
282
|
+
]);
|
|
283
|
+
var METRIC_UNITS = Object.freeze({
|
|
284
|
+
"kaji.turn.queue_wait_ms": "ms",
|
|
285
|
+
"kaji.turn.duration_ms": "ms",
|
|
286
|
+
"kaji.turn.iterations": "count",
|
|
287
|
+
"kaji.provider.duration_ms": "ms",
|
|
288
|
+
"kaji.provider.retries": "count",
|
|
289
|
+
"kaji.replay.input_events": "count",
|
|
290
|
+
"kaji.context.messages": "count",
|
|
291
|
+
"kaji.context.characters": "count",
|
|
292
|
+
"kaji.tool.queue_wait_ms": "ms",
|
|
293
|
+
"kaji.tool.active": "gauge",
|
|
294
|
+
"kaji.tool.duration_ms": "ms",
|
|
295
|
+
"kaji.journal.failures": "count",
|
|
296
|
+
"kaji.subscriber.lag_events": "count",
|
|
297
|
+
"kaji.subscriber.overflow": "count",
|
|
298
|
+
"kaji.integration.auth_ms": "ms",
|
|
299
|
+
"kaji.integration.request_ms": "ms"
|
|
300
|
+
});
|
|
301
|
+
Object.freeze(
|
|
302
|
+
Object.keys(METRIC_UNITS)
|
|
303
|
+
);
|
|
304
|
+
var ALLOWED_LABELS = Object.freeze({
|
|
305
|
+
"kaji.turn.queue_wait_ms": [],
|
|
306
|
+
"kaji.turn.duration_ms": ["outcome"],
|
|
307
|
+
"kaji.turn.iterations": ["outcome"],
|
|
308
|
+
"kaji.provider.duration_ms": ["provider_family", "status"],
|
|
309
|
+
"kaji.provider.retries": ["provider_family"],
|
|
310
|
+
"kaji.replay.input_events": [],
|
|
311
|
+
"kaji.context.messages": [],
|
|
312
|
+
"kaji.context.characters": [],
|
|
313
|
+
"kaji.tool.queue_wait_ms": ["outcome"],
|
|
314
|
+
"kaji.tool.active": [],
|
|
315
|
+
"kaji.tool.duration_ms": ["outcome", "error_code"],
|
|
316
|
+
"kaji.journal.failures": ["stage"],
|
|
317
|
+
"kaji.subscriber.lag_events": [],
|
|
318
|
+
"kaji.subscriber.overflow": ["stage"],
|
|
319
|
+
"kaji.integration.auth_ms": ["integration", "operation", "outcome"],
|
|
320
|
+
"kaji.integration.request_ms": ["integration", "operation", "outcome"]
|
|
321
|
+
});
|
|
322
|
+
var STABLE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
323
|
+
"NONE",
|
|
324
|
+
"OTHER",
|
|
325
|
+
"INVALID_TOOL_SCHEMA",
|
|
326
|
+
"INVALID_TOOL_ARGUMENTS",
|
|
327
|
+
"UNCLASSIFIED_TOOL_RISK",
|
|
328
|
+
"MISSING_TOOL_IDENTITY",
|
|
329
|
+
"TOOL_NOT_ALLOWED",
|
|
330
|
+
"APPROVAL_UNAVAILABLE",
|
|
331
|
+
"APPROVAL_REJECTED",
|
|
332
|
+
"APPROVAL_TIMEOUT",
|
|
333
|
+
"TOOL_CANCELLED",
|
|
334
|
+
"TOOL_TIMEOUT",
|
|
335
|
+
"TURN_TIMEOUT",
|
|
336
|
+
"TOOL_EXECUTION_FAILED",
|
|
337
|
+
"TOOL_START_RECORD_FAILED",
|
|
338
|
+
"IDEMPOTENCY_CAPACITY_EXCEEDED",
|
|
339
|
+
"IDEMPOTENCY_CONFLICT",
|
|
340
|
+
"INTEGRATION_AUTH_ERROR",
|
|
341
|
+
"INTEGRATION_AUTH_REQUIRED",
|
|
342
|
+
"INTEGRATION_API_ERROR",
|
|
343
|
+
"INTEGRATION_POLICY_REJECTED",
|
|
344
|
+
"INTEGRATION_RATE_LIMITED",
|
|
345
|
+
"INTEGRATION_REDIRECT_REJECTED",
|
|
346
|
+
"INTEGRATION_RESPONSE_LIMIT"
|
|
347
|
+
]);
|
|
348
|
+
function validLabel(key, value) {
|
|
349
|
+
if (typeof value !== "string") return false;
|
|
350
|
+
const text = value;
|
|
351
|
+
if (key === "provider_family") {
|
|
352
|
+
return text === "openai" || text === "anthropic" || text === "custom";
|
|
353
|
+
}
|
|
354
|
+
if (key === "status") {
|
|
355
|
+
return text === "success" || text === "cancelled" || text === "error";
|
|
356
|
+
}
|
|
357
|
+
if (key === "stage") {
|
|
358
|
+
return ["append", "publish", "lag", "overflow"].includes(text);
|
|
359
|
+
}
|
|
360
|
+
if (key === "error_code") return STABLE_ERROR_CODES.has(text);
|
|
361
|
+
if (key === "outcome") {
|
|
362
|
+
return [
|
|
363
|
+
"acquired",
|
|
364
|
+
"completed",
|
|
365
|
+
"failed",
|
|
366
|
+
"cancelled",
|
|
367
|
+
"timeout",
|
|
368
|
+
"not_started",
|
|
369
|
+
"unknown",
|
|
370
|
+
"success",
|
|
371
|
+
"error"
|
|
372
|
+
].includes(text);
|
|
373
|
+
}
|
|
374
|
+
if (key === "integration") return text === "github" || text === "gmail";
|
|
375
|
+
if (key === "operation") return ["read", "mutation", "token"].includes(text);
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
function recordMetric(sink, name, value, labels) {
|
|
379
|
+
if (!Number.isFinite(value)) return;
|
|
380
|
+
if (sink === NOOP_METRICS) return;
|
|
381
|
+
if (!Object.hasOwn(METRIC_UNITS, name)) return;
|
|
382
|
+
const sanitized = {};
|
|
383
|
+
const allowed = ALLOWED_LABELS[name];
|
|
384
|
+
const supplied = Object.keys(labels);
|
|
385
|
+
if (supplied.length !== allowed.length || supplied.some((key) => !allowed.includes(key))) return;
|
|
386
|
+
for (const key of allowed) {
|
|
387
|
+
const raw = labels[key];
|
|
388
|
+
if (!validLabel(key, raw)) return;
|
|
389
|
+
sanitized[key] = raw;
|
|
390
|
+
}
|
|
391
|
+
const measurement = Object.freeze({
|
|
392
|
+
name,
|
|
393
|
+
value,
|
|
394
|
+
unit: METRIC_UNITS[name],
|
|
395
|
+
labels: Object.freeze(sanitized)
|
|
396
|
+
});
|
|
397
|
+
try {
|
|
398
|
+
const pending = sink.record(measurement);
|
|
399
|
+
if (pending !== void 0) void Promise.resolve(pending).catch(() => void 0);
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
var NOOP_SPAN = Object.freeze({
|
|
404
|
+
setAttribute: () => {
|
|
405
|
+
},
|
|
406
|
+
recordError: () => {
|
|
407
|
+
},
|
|
408
|
+
end: () => {
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
var NOOP_METRICS = Object.freeze({ record: () => {
|
|
412
|
+
} });
|
|
413
|
+
var NOOP_TRACE = Object.freeze({ startSpan: () => NOOP_SPAN });
|
|
414
|
+
function validTraceAttribute(name, value) {
|
|
415
|
+
if (typeof value !== "string") return false;
|
|
416
|
+
if (name === "integration.name") return value === "github" || value === "gmail";
|
|
417
|
+
if (name === "integration.operation") {
|
|
418
|
+
return value === "read" || value === "mutation" || value === "token";
|
|
419
|
+
}
|
|
420
|
+
if (name === "http.status_family") return /^(?:[1-5]xx|none)$/.test(value);
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
function startSpan(sink, name, attributes = {}) {
|
|
424
|
+
if (sink === NOOP_TRACE) return NOOP_SPAN;
|
|
425
|
+
if (!SPAN_NAMES.has(name)) return NOOP_SPAN;
|
|
426
|
+
let safeAttributes;
|
|
427
|
+
try {
|
|
428
|
+
const entries = Object.entries(attributes);
|
|
429
|
+
if (entries.some(
|
|
430
|
+
([key, value]) => !TRACE_ATTRIBUTES.has(key) || !validTraceAttribute(key, value)
|
|
431
|
+
)) {
|
|
432
|
+
return NOOP_SPAN;
|
|
433
|
+
}
|
|
434
|
+
safeAttributes = Object.freeze(Object.fromEntries(entries));
|
|
435
|
+
} catch {
|
|
436
|
+
return NOOP_SPAN;
|
|
437
|
+
}
|
|
438
|
+
let inner;
|
|
439
|
+
try {
|
|
440
|
+
inner = sink.startSpan(name, safeAttributes);
|
|
441
|
+
} catch {
|
|
442
|
+
return NOOP_SPAN;
|
|
443
|
+
}
|
|
444
|
+
let ended = false;
|
|
445
|
+
return {
|
|
446
|
+
setAttribute(attributeName, value) {
|
|
447
|
+
if (ended) return;
|
|
448
|
+
if (!TRACE_ATTRIBUTES.has(attributeName) || !validTraceAttribute(attributeName, value))
|
|
449
|
+
return;
|
|
450
|
+
try {
|
|
451
|
+
inner.setAttribute(attributeName, value);
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
recordError(error) {
|
|
456
|
+
if (ended) return;
|
|
457
|
+
try {
|
|
458
|
+
const kind = error instanceof Error ? error.name : typeof error;
|
|
459
|
+
inner.recordError(new Error(`${kind}: details redacted`));
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
end() {
|
|
464
|
+
if (ended) return;
|
|
465
|
+
ended = true;
|
|
466
|
+
try {
|
|
467
|
+
inner.end();
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/auth/oauth.ts
|
|
475
|
+
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
476
|
+
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
477
|
+
var GOOGLE_REVOKE_URL = "https://oauth2.googleapis.com/revoke";
|
|
478
|
+
var PRINCIPAL = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
479
|
+
var MAX_CREDENTIAL_BYTES = 16 * 1024;
|
|
480
|
+
var MAX_PROVIDER_BYTES = 64 * 1024;
|
|
481
|
+
var MAX_TOKEN_CHARACTERS = 8192;
|
|
482
|
+
var REFRESH_BUFFER_MS = 6e4;
|
|
483
|
+
var OPERATION_MS = 3e4;
|
|
484
|
+
var CALLBACK_MS = 5 * 6e4;
|
|
485
|
+
var OAuthOperationError = class extends Error {
|
|
486
|
+
constructor() {
|
|
487
|
+
super("OAuth operation failed");
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
var OAuthScopeDriftError = class extends OAuthOperationError {
|
|
491
|
+
};
|
|
492
|
+
var Mutex = class {
|
|
493
|
+
tail = Promise.resolve();
|
|
494
|
+
async run(operation) {
|
|
495
|
+
const previous = this.tail;
|
|
496
|
+
const next = Promise.withResolvers();
|
|
497
|
+
this.tail = next.promise;
|
|
498
|
+
await previous;
|
|
499
|
+
try {
|
|
500
|
+
return await operation();
|
|
501
|
+
} finally {
|
|
502
|
+
next.resolve();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
function policyError() {
|
|
507
|
+
return new IntegrationPolicyError();
|
|
508
|
+
}
|
|
509
|
+
function authRequired(reason) {
|
|
510
|
+
return new IntegrationAuthRequiredError(reason);
|
|
511
|
+
}
|
|
512
|
+
function validateOAuthPrincipal(value) {
|
|
513
|
+
if (typeof value !== "string" || !PRINCIPAL.test(value)) throw policyError();
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
function scalarLength(value) {
|
|
517
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
518
|
+
const unit = value.charCodeAt(index);
|
|
519
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
520
|
+
const next = value.charCodeAt(index + 1);
|
|
521
|
+
if (next < 56320 || next > 57343) throw new OAuthOperationError();
|
|
522
|
+
index += 1;
|
|
523
|
+
} else if (unit >= 56320 && unit <= 57343) {
|
|
524
|
+
throw new OAuthOperationError();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return Array.from(value).length;
|
|
528
|
+
}
|
|
529
|
+
function boundedString(value, maximum = MAX_TOKEN_CHARACTERS) {
|
|
530
|
+
if (typeof value !== "string" || scalarLength(value) < 1 || scalarLength(value) > maximum) {
|
|
531
|
+
throw new OAuthOperationError();
|
|
532
|
+
}
|
|
533
|
+
return value;
|
|
534
|
+
}
|
|
535
|
+
function normalizedScopes(value, provider = false) {
|
|
536
|
+
if (!Array.isArray(value)) throw provider ? new OAuthOperationError() : policyError();
|
|
537
|
+
const scopes = [...new Set(value)].sort();
|
|
538
|
+
if (scopes.length === 0 || scopes.length > 64 || scopes.some(
|
|
539
|
+
(scope) => typeof scope !== "string" || scope.length === 0 || scalarLength(scope) > 2048 || /\s/.test(scope)
|
|
540
|
+
)) {
|
|
541
|
+
throw provider ? new OAuthOperationError() : policyError();
|
|
542
|
+
}
|
|
543
|
+
return Object.freeze(scopes);
|
|
544
|
+
}
|
|
545
|
+
function snapshotOAuthCredentialRecord(value) {
|
|
546
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
547
|
+
throw new OAuthOperationError();
|
|
548
|
+
}
|
|
549
|
+
const root = value;
|
|
550
|
+
if (Object.keys(root).sort().join(",") !== "schemaVersion,state,tokens") {
|
|
551
|
+
throw new OAuthOperationError();
|
|
552
|
+
}
|
|
553
|
+
if (root.schemaVersion !== 1 || root.state !== "active" && root.state !== "revocation_pending") {
|
|
554
|
+
throw new OAuthOperationError();
|
|
555
|
+
}
|
|
556
|
+
if (root.tokens === null || typeof root.tokens !== "object" || Array.isArray(root.tokens)) {
|
|
557
|
+
throw new OAuthOperationError();
|
|
558
|
+
}
|
|
559
|
+
const tokens = root.tokens;
|
|
560
|
+
if (Object.keys(tokens).sort().join(",") !== "accessToken,expiresAtEpochMs,grantedScopes,refreshToken,tokenType") {
|
|
561
|
+
throw new OAuthOperationError();
|
|
562
|
+
}
|
|
563
|
+
if (tokens.tokenType !== "Bearer" || !Number.isSafeInteger(tokens.expiresAtEpochMs) || tokens.expiresAtEpochMs < 1 || !Array.isArray(tokens.grantedScopes)) {
|
|
564
|
+
throw new OAuthOperationError();
|
|
565
|
+
}
|
|
566
|
+
const grantedScopes = normalizedScopes(tokens.grantedScopes, true);
|
|
567
|
+
if (grantedScopes.length !== tokens.grantedScopes.length) throw new OAuthOperationError();
|
|
568
|
+
const record = {
|
|
569
|
+
schemaVersion: 1,
|
|
570
|
+
state: root.state,
|
|
571
|
+
tokens: {
|
|
572
|
+
accessToken: boundedString(tokens.accessToken),
|
|
573
|
+
refreshToken: boundedString(tokens.refreshToken),
|
|
574
|
+
expiresAtEpochMs: tokens.expiresAtEpochMs,
|
|
575
|
+
grantedScopes,
|
|
576
|
+
tokenType: "Bearer"
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
return Object.freeze({ ...record, tokens: Object.freeze(record.tokens) });
|
|
580
|
+
}
|
|
581
|
+
function canonicalOAuthCredentialJson(value) {
|
|
582
|
+
const record = snapshotOAuthCredentialRecord(value);
|
|
583
|
+
const encoded = JSON.stringify({
|
|
584
|
+
schemaVersion: 1,
|
|
585
|
+
state: record.state,
|
|
586
|
+
tokens: {
|
|
587
|
+
accessToken: record.tokens.accessToken,
|
|
588
|
+
expiresAtEpochMs: record.tokens.expiresAtEpochMs,
|
|
589
|
+
grantedScopes: record.tokens.grantedScopes,
|
|
590
|
+
refreshToken: record.tokens.refreshToken,
|
|
591
|
+
tokenType: "Bearer"
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
if (new TextEncoder().encode(encoded).byteLength > MAX_CREDENTIAL_BYTES) {
|
|
595
|
+
throw new OAuthOperationError();
|
|
596
|
+
}
|
|
597
|
+
return encoded;
|
|
598
|
+
}
|
|
599
|
+
function parseProviderJson(bytes) {
|
|
600
|
+
if (bytes.byteLength > MAX_PROVIDER_BYTES) throw new OAuthOperationError();
|
|
601
|
+
try {
|
|
602
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
603
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
604
|
+
throw new OAuthOperationError();
|
|
605
|
+
}
|
|
606
|
+
return value;
|
|
607
|
+
} catch {
|
|
608
|
+
throw new OAuthOperationError();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function providerErrorCode(bytes) {
|
|
612
|
+
try {
|
|
613
|
+
const error = parseProviderJson(bytes).error;
|
|
614
|
+
return error === "invalid_grant" ? error : void 0;
|
|
615
|
+
} catch {
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function abortError() {
|
|
620
|
+
return new DOMException("OAuth operation cancelled", "AbortError");
|
|
621
|
+
}
|
|
622
|
+
function timeoutError() {
|
|
623
|
+
return new DOMException("OAuth operation timed out", "TimeoutError");
|
|
624
|
+
}
|
|
625
|
+
function throwIfAborted(signal) {
|
|
626
|
+
if (signal.aborted) throw abortError();
|
|
627
|
+
}
|
|
628
|
+
var NodeOAuthTransport = class {
|
|
629
|
+
postForm(endpoint, form, signal, deadlineMonotonicMs) {
|
|
630
|
+
if (endpoint !== GOOGLE_TOKEN_URL && endpoint !== GOOGLE_REVOKE_URL) throw policyError();
|
|
631
|
+
throwIfAborted(signal);
|
|
632
|
+
const remaining = Math.min(OPERATION_MS, deadlineMonotonicMs - performance.now());
|
|
633
|
+
if (remaining <= 0) return Promise.reject(timeoutError());
|
|
634
|
+
return new Promise((resolve, reject) => {
|
|
635
|
+
const body = new URLSearchParams(form).toString();
|
|
636
|
+
const request = https.request(
|
|
637
|
+
endpoint,
|
|
638
|
+
{
|
|
639
|
+
method: "POST",
|
|
640
|
+
headers: {
|
|
641
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
642
|
+
"content-length": Buffer.byteLength(body)
|
|
643
|
+
},
|
|
644
|
+
timeout: remaining
|
|
645
|
+
},
|
|
646
|
+
(response) => {
|
|
647
|
+
const chunks = [];
|
|
648
|
+
let size = 0;
|
|
649
|
+
response.on("data", (chunk) => {
|
|
650
|
+
size += chunk.byteLength;
|
|
651
|
+
if (size > MAX_PROVIDER_BYTES) request.destroy(new OAuthOperationError());
|
|
652
|
+
else chunks.push(chunk);
|
|
653
|
+
});
|
|
654
|
+
response.once(
|
|
655
|
+
"end",
|
|
656
|
+
() => resolve({ status: response.statusCode ?? 0, bytes: Buffer.concat(chunks) })
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
);
|
|
660
|
+
const onAbort = () => request.destroy(abortError());
|
|
661
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
662
|
+
const absoluteTimeout = setTimeout(() => request.destroy(timeoutError()), remaining);
|
|
663
|
+
request.once("timeout", () => request.destroy(timeoutError()));
|
|
664
|
+
request.once(
|
|
665
|
+
"error",
|
|
666
|
+
(error) => reject(error instanceof DOMException ? error : new OAuthOperationError())
|
|
667
|
+
);
|
|
668
|
+
request.once("close", () => {
|
|
669
|
+
clearTimeout(absoluteTimeout);
|
|
670
|
+
signal.removeEventListener("abort", onAbort);
|
|
671
|
+
});
|
|
672
|
+
request.end(body);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
var LoopbackCallback = class {
|
|
677
|
+
constructor(server, port, sockets) {
|
|
678
|
+
this.server = server;
|
|
679
|
+
this.sockets = sockets;
|
|
680
|
+
this.redirectUri = `http://127.0.0.1:${port}/oauth/callback`;
|
|
681
|
+
}
|
|
682
|
+
server;
|
|
683
|
+
sockets;
|
|
684
|
+
redirectUri;
|
|
685
|
+
result = Promise.withResolvers();
|
|
686
|
+
accept(target) {
|
|
687
|
+
try {
|
|
688
|
+
const url = new URL(target, this.redirectUri);
|
|
689
|
+
const code = url.searchParams.has("error") ? "" : url.searchParams.get("code") ?? "";
|
|
690
|
+
this.result.resolve([
|
|
691
|
+
(url.searchParams.get("state") ?? "").slice(0, 256),
|
|
692
|
+
code.slice(0, 8192)
|
|
693
|
+
]);
|
|
694
|
+
return code.length > 0;
|
|
695
|
+
} catch {
|
|
696
|
+
this.result.resolve(["", ""]);
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
async waitForCode(expectedState, signal, deadlineMonotonicMs) {
|
|
701
|
+
const [state, code] = await raceSignal(this.result.promise, signal, deadlineMonotonicMs);
|
|
702
|
+
const left = Buffer.from(state);
|
|
703
|
+
const right = Buffer.from(expectedState);
|
|
704
|
+
const leftHash = crypto.createHash("sha256").update(left).digest();
|
|
705
|
+
const rightHash = crypto.createHash("sha256").update(right).digest();
|
|
706
|
+
if (left.byteLength !== right.byteLength || !crypto.timingSafeEqual(leftHash, rightHash)) {
|
|
707
|
+
throw new OAuthOperationError();
|
|
708
|
+
}
|
|
709
|
+
return boundedString(code);
|
|
710
|
+
}
|
|
711
|
+
async close() {
|
|
712
|
+
for (const socket of this.sockets) socket.destroy();
|
|
713
|
+
if (!this.server.listening) return;
|
|
714
|
+
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
var LoopbackCallbackFactory = class {
|
|
718
|
+
open(signal, deadlineMonotonicMs) {
|
|
719
|
+
throwIfAborted(signal);
|
|
720
|
+
const remaining = deadlineMonotonicMs - performance.now();
|
|
721
|
+
if (remaining <= 0) return Promise.reject(timeoutError());
|
|
722
|
+
return new Promise((resolve, reject) => {
|
|
723
|
+
let callback;
|
|
724
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
725
|
+
const server = http.createServer((request, response) => {
|
|
726
|
+
if (request.method !== "GET" || request.url === void 0 || request.url.length > 8192) {
|
|
727
|
+
response.writeHead(400).end();
|
|
728
|
+
callback?.accept("");
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
let validPath = false;
|
|
732
|
+
try {
|
|
733
|
+
validPath = new URL(request.url, "http://127.0.0.1").pathname === "/oauth/callback";
|
|
734
|
+
} catch {
|
|
735
|
+
}
|
|
736
|
+
const accepted = validPath && callback !== void 0 && callback.accept(request.url);
|
|
737
|
+
response.writeHead(accepted ? 200 : 400, { "content-length": "0", connection: "close" }).end();
|
|
738
|
+
});
|
|
739
|
+
server.on("connection", (socket) => {
|
|
740
|
+
sockets.add(socket);
|
|
741
|
+
socket.once("close", () => sockets.delete(socket));
|
|
742
|
+
});
|
|
743
|
+
let settled = false;
|
|
744
|
+
const cleanup = () => {
|
|
745
|
+
clearTimeout(timeout);
|
|
746
|
+
signal.removeEventListener("abort", onAbort);
|
|
747
|
+
};
|
|
748
|
+
const fail = (error) => {
|
|
749
|
+
if (settled) return;
|
|
750
|
+
settled = true;
|
|
751
|
+
cleanup();
|
|
752
|
+
try {
|
|
753
|
+
server.close();
|
|
754
|
+
} catch {
|
|
755
|
+
}
|
|
756
|
+
reject(error);
|
|
757
|
+
};
|
|
758
|
+
const onAbort = () => fail(abortError());
|
|
759
|
+
const timeout = setTimeout(() => fail(timeoutError()), remaining);
|
|
760
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
761
|
+
server.once("error", () => fail(new OAuthOperationError()));
|
|
762
|
+
server.listen(0, "127.0.0.1", () => {
|
|
763
|
+
if (settled) {
|
|
764
|
+
for (const socket of sockets) socket.destroy();
|
|
765
|
+
server.close();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const address = server.address();
|
|
769
|
+
if (address === null || typeof address === "string") {
|
|
770
|
+
fail(new OAuthOperationError());
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
settled = true;
|
|
774
|
+
cleanup();
|
|
775
|
+
callback = new LoopbackCallback(server, address.port, sockets);
|
|
776
|
+
resolve(callback);
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
var SystemBrowser = class {
|
|
782
|
+
async open(url, signal, deadlineMonotonicMs) {
|
|
783
|
+
throwIfAborted(signal);
|
|
784
|
+
const remaining = deadlineMonotonicMs - performance.now();
|
|
785
|
+
if (remaining <= 0) throw timeoutError();
|
|
786
|
+
await new Promise((resolve, reject) => {
|
|
787
|
+
const child = child_process.spawn("/usr/bin/open", [url], { stdio: "ignore", shell: false });
|
|
788
|
+
let failure;
|
|
789
|
+
let grace;
|
|
790
|
+
const terminate = (error) => {
|
|
791
|
+
failure ??= error;
|
|
792
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
793
|
+
child.kill("SIGTERM");
|
|
794
|
+
grace ??= setTimeout(() => {
|
|
795
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
796
|
+
}, 250);
|
|
797
|
+
};
|
|
798
|
+
const onAbort = () => terminate(abortError());
|
|
799
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
800
|
+
const timeout = setTimeout(() => terminate(timeoutError()), remaining);
|
|
801
|
+
child.once("error", () => terminate(new OAuthOperationError()));
|
|
802
|
+
child.once("close", (code) => {
|
|
803
|
+
clearTimeout(timeout);
|
|
804
|
+
if (grace !== void 0) clearTimeout(grace);
|
|
805
|
+
signal.removeEventListener("abort", onAbort);
|
|
806
|
+
if (failure !== void 0) reject(failure);
|
|
807
|
+
else if (code === 0) resolve();
|
|
808
|
+
else reject(new OAuthOperationError());
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
async function raceSignal(operation, signal, deadlineMonotonicMs, nowMonotonic = () => performance.now()) {
|
|
814
|
+
throwIfAborted(signal);
|
|
815
|
+
let remaining;
|
|
816
|
+
if (deadlineMonotonicMs !== void 0) {
|
|
817
|
+
remaining = deadlineMonotonicMs - nowMonotonic();
|
|
818
|
+
if (remaining <= 0) throw timeoutError();
|
|
819
|
+
}
|
|
820
|
+
const cancellation = Promise.withResolvers();
|
|
821
|
+
const onAbort = () => cancellation.reject(abortError());
|
|
822
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
823
|
+
let timer;
|
|
824
|
+
const candidates = [operation, cancellation.promise];
|
|
825
|
+
if (remaining !== void 0) {
|
|
826
|
+
const timeout = new Promise((_, reject) => {
|
|
827
|
+
timer = setTimeout(() => reject(timeoutError()), remaining);
|
|
828
|
+
});
|
|
829
|
+
candidates.push(timeout);
|
|
830
|
+
}
|
|
831
|
+
try {
|
|
832
|
+
return await Promise.race(candidates);
|
|
833
|
+
} finally {
|
|
834
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
835
|
+
signal.removeEventListener("abort", onAbort);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function base64Url(value) {
|
|
839
|
+
return Buffer.from(value).toString("base64url");
|
|
840
|
+
}
|
|
841
|
+
var GoogleOAuthClient = class _GoogleOAuthClient {
|
|
842
|
+
options;
|
|
843
|
+
dependencies;
|
|
844
|
+
metrics;
|
|
845
|
+
trace;
|
|
846
|
+
slots = /* @__PURE__ */ new Map();
|
|
847
|
+
slotsGate = new Mutex();
|
|
848
|
+
constructor(options) {
|
|
849
|
+
this.options = validatedOptions(options);
|
|
850
|
+
this.metrics = options.metricsSink ?? NOOP_METRICS;
|
|
851
|
+
this.trace = options.traceSink ?? NOOP_TRACE;
|
|
852
|
+
this.dependencies = {
|
|
853
|
+
http: new NodeOAuthTransport(),
|
|
854
|
+
callbackFactory: new LoopbackCallbackFactory(),
|
|
855
|
+
browser: new SystemBrowser(),
|
|
856
|
+
clock: systemClock,
|
|
857
|
+
randomBytes: (count) => crypto.randomBytes(count)
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
/** @internal Source-relative tests only. */
|
|
861
|
+
static _create(options, dependencies) {
|
|
862
|
+
const client = Object.create(_GoogleOAuthClient.prototype);
|
|
863
|
+
Object.defineProperty(client, "options", { value: validatedOptions(options) });
|
|
864
|
+
Object.defineProperty(client, "dependencies", { value: dependencies });
|
|
865
|
+
Object.defineProperty(client, "metrics", { value: options.metricsSink ?? NOOP_METRICS });
|
|
866
|
+
Object.defineProperty(client, "trace", { value: options.traceSink ?? NOOP_TRACE });
|
|
867
|
+
Object.defineProperty(client, "slots", { value: /* @__PURE__ */ new Map() });
|
|
868
|
+
Object.defineProperty(client, "slotsGate", { value: new Mutex() });
|
|
869
|
+
return client;
|
|
870
|
+
}
|
|
871
|
+
async connect(principalId, signal) {
|
|
872
|
+
principalId = validateOAuthPrincipal(principalId);
|
|
873
|
+
throwIfAborted(signal);
|
|
874
|
+
const clientId = this.requiredClientId();
|
|
875
|
+
const slot = await this.acquireSlot(principalId);
|
|
876
|
+
try {
|
|
877
|
+
let operation;
|
|
878
|
+
while (operation === void 0) {
|
|
879
|
+
let pendingDisconnect;
|
|
880
|
+
await slot.gate.run(() => {
|
|
881
|
+
if (slot.disconnect !== void 0) {
|
|
882
|
+
pendingDisconnect = slot.disconnect;
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
slot.generation += 1;
|
|
886
|
+
slot.blocked = true;
|
|
887
|
+
const previous = [];
|
|
888
|
+
if (slot.connect !== void 0) {
|
|
889
|
+
slot.connect.controller.abort(abortError());
|
|
890
|
+
previous.push(slot.connect.promise);
|
|
891
|
+
}
|
|
892
|
+
if (slot.refresh !== void 0) {
|
|
893
|
+
slot.refresh.controller.abort(abortError());
|
|
894
|
+
previous.push(slot.refresh.promise);
|
|
895
|
+
}
|
|
896
|
+
const controller = new AbortController();
|
|
897
|
+
const generation = slot.generation;
|
|
898
|
+
const promise = (async () => {
|
|
899
|
+
await Promise.allSettled(previous);
|
|
900
|
+
throwIfAborted(controller.signal);
|
|
901
|
+
await slot.gate.run(() => {
|
|
902
|
+
if (slot.generation !== generation || slot.connect?.generation !== generation || slot.connect.controller !== controller) {
|
|
903
|
+
throw authRequired("gmail_grant_missing");
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
await this.runConnect(principalId, slot, generation, clientId, controller.signal);
|
|
907
|
+
await slot.gate.run(() => {
|
|
908
|
+
if (slot.generation !== generation || slot.connect?.generation !== generation || slot.connect.controller !== controller) {
|
|
909
|
+
throw authRequired("gmail_grant_missing");
|
|
910
|
+
}
|
|
911
|
+
slot.blocked = false;
|
|
912
|
+
});
|
|
913
|
+
})();
|
|
914
|
+
operation = { generation, controller, promise };
|
|
915
|
+
slot.connect = operation;
|
|
916
|
+
});
|
|
917
|
+
if (pendingDisconnect !== void 0) await raceSignal(pendingDisconnect, signal);
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
await raceSignal(operation.promise, signal);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
operation.controller.abort(abortError());
|
|
923
|
+
await Promise.allSettled([operation.promise]);
|
|
924
|
+
throw error;
|
|
925
|
+
} finally {
|
|
926
|
+
await slot.gate.run(() => {
|
|
927
|
+
if (slot.connect === operation) delete slot.connect;
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
} finally {
|
|
931
|
+
await this.releaseSlot(principalId, slot);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async accessToken(context) {
|
|
935
|
+
const started = this.dependencies.clock.nowMonotonic();
|
|
936
|
+
const span = startSpan(this.trace, "kaji.integration.auth", {
|
|
937
|
+
"integration.name": "gmail",
|
|
938
|
+
"integration.operation": "token",
|
|
939
|
+
"http.status_family": "none"
|
|
940
|
+
});
|
|
941
|
+
let outcome = "error";
|
|
942
|
+
try {
|
|
943
|
+
const token = await this.accessTokenInternal(context);
|
|
944
|
+
outcome = "success";
|
|
945
|
+
return token;
|
|
946
|
+
} catch (error) {
|
|
947
|
+
if (error instanceof DOMException && error.name === "AbortError") outcome = "cancelled";
|
|
948
|
+
span.recordError(error);
|
|
949
|
+
throw error;
|
|
950
|
+
} finally {
|
|
951
|
+
recordMetric(
|
|
952
|
+
this.metrics,
|
|
953
|
+
"kaji.integration.auth_ms",
|
|
954
|
+
Math.max(0, this.dependencies.clock.nowMonotonic() - started),
|
|
955
|
+
{ integration: "gmail", operation: "token", outcome }
|
|
956
|
+
);
|
|
957
|
+
span.end();
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async accessTokenInternal(context) {
|
|
961
|
+
const principalId = validateOAuthPrincipal(context.principalId);
|
|
962
|
+
throwIfAborted(context.signal);
|
|
963
|
+
this.checkDeadline(context.deadlineMonotonicMs);
|
|
964
|
+
const slot = await this.acquireSlot(principalId);
|
|
965
|
+
try {
|
|
966
|
+
if (slot.blocked) throw authRequired("gmail_grant_missing");
|
|
967
|
+
const generation = await slot.gate.run(() => {
|
|
968
|
+
if (slot.blocked) throw authRequired("gmail_grant_missing");
|
|
969
|
+
return slot.generation;
|
|
970
|
+
});
|
|
971
|
+
const loaded = await raceSignal(
|
|
972
|
+
this.options.storage.load(principalId, context.signal),
|
|
973
|
+
context.signal,
|
|
974
|
+
context.deadlineMonotonicMs,
|
|
975
|
+
() => this.dependencies.clock.nowMonotonic()
|
|
976
|
+
);
|
|
977
|
+
if (loaded === void 0) throw authRequired("gmail_grant_missing");
|
|
978
|
+
const record = snapshotOAuthCredentialRecord(loaded);
|
|
979
|
+
if (record.state === "revocation_pending") throw authRequired("gmail_grant_missing");
|
|
980
|
+
if (!isSuperset(record.tokens.grantedScopes, this.options.scopes)) {
|
|
981
|
+
this.checkDeadline(context.deadlineMonotonicMs);
|
|
982
|
+
await this.deleteIfCurrent(principalId, slot, generation, context.signal);
|
|
983
|
+
throw authRequired("gmail_scope_drift");
|
|
984
|
+
}
|
|
985
|
+
if (record.tokens.expiresAtEpochMs > this.dependencies.clock.nowWallSeconds() * 1e3 + REFRESH_BUFFER_MS) {
|
|
986
|
+
return slot.gate.run(() => {
|
|
987
|
+
this.checkDeadline(context.deadlineMonotonicMs);
|
|
988
|
+
if (slot.blocked || slot.generation !== generation) {
|
|
989
|
+
throw authRequired("gmail_grant_missing");
|
|
990
|
+
}
|
|
991
|
+
return record.tokens.accessToken;
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
return await this.joinRefresh(principalId, slot, generation, record, context);
|
|
995
|
+
} finally {
|
|
996
|
+
await this.releaseSlot(principalId, slot);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async disconnect(principalId, signal, options = {}) {
|
|
1000
|
+
principalId = validateOAuthPrincipal(principalId);
|
|
1001
|
+
throwIfAborted(signal);
|
|
1002
|
+
const slot = await this.acquireSlot(principalId);
|
|
1003
|
+
const completion = Promise.withResolvers();
|
|
1004
|
+
let previousDisconnect;
|
|
1005
|
+
await slot.gate.run(() => {
|
|
1006
|
+
previousDisconnect = slot.disconnect;
|
|
1007
|
+
slot.disconnect = completion.promise;
|
|
1008
|
+
});
|
|
1009
|
+
try {
|
|
1010
|
+
if (previousDisconnect !== void 0) await raceSignal(previousDisconnect, signal);
|
|
1011
|
+
let generation = 0;
|
|
1012
|
+
let operations = [];
|
|
1013
|
+
await slot.gate.run(() => {
|
|
1014
|
+
slot.generation += 1;
|
|
1015
|
+
generation = slot.generation;
|
|
1016
|
+
slot.blocked = true;
|
|
1017
|
+
if (slot.connect !== void 0) {
|
|
1018
|
+
slot.connect.controller.abort(abortError());
|
|
1019
|
+
operations.push(slot.connect.promise);
|
|
1020
|
+
}
|
|
1021
|
+
if (slot.refresh !== void 0) {
|
|
1022
|
+
slot.refresh.controller.abort(abortError());
|
|
1023
|
+
operations.push(slot.refresh.promise);
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
await Promise.allSettled(operations);
|
|
1027
|
+
const loaded = await this.options.storage.load(principalId, signal);
|
|
1028
|
+
if (loaded === void 0)
|
|
1029
|
+
return Object.freeze({ localState: "missing", remoteRevoked: false });
|
|
1030
|
+
const record = snapshotOAuthCredentialRecord(loaded);
|
|
1031
|
+
if (options.forceLocal === true) {
|
|
1032
|
+
await this.deleteBlockedIfCurrent(principalId, slot, generation, signal);
|
|
1033
|
+
return Object.freeze({ localState: "deleted", remoteRevoked: false });
|
|
1034
|
+
}
|
|
1035
|
+
const pending = snapshotOAuthCredentialRecord({ ...record, state: "revocation_pending" });
|
|
1036
|
+
const cleanup = new AbortController();
|
|
1037
|
+
let response;
|
|
1038
|
+
try {
|
|
1039
|
+
response = await this.dependencies.http.postForm(
|
|
1040
|
+
GOOGLE_REVOKE_URL,
|
|
1041
|
+
{ token: record.tokens.refreshToken },
|
|
1042
|
+
signal,
|
|
1043
|
+
this.dependencies.clock.nowMonotonic() + OPERATION_MS
|
|
1044
|
+
);
|
|
1045
|
+
} catch {
|
|
1046
|
+
await this.saveIfCurrent(principalId, slot, generation, pending, cleanup.signal);
|
|
1047
|
+
if (signal.aborted) throw abortError();
|
|
1048
|
+
return Object.freeze({ localState: "revocation_pending", remoteRevoked: false });
|
|
1049
|
+
}
|
|
1050
|
+
if (response.status === 200) {
|
|
1051
|
+
try {
|
|
1052
|
+
await this.deleteBlockedIfCurrent(principalId, slot, generation, cleanup.signal);
|
|
1053
|
+
} catch {
|
|
1054
|
+
await this.saveIfCurrent(principalId, slot, generation, pending, cleanup.signal);
|
|
1055
|
+
return Object.freeze({ localState: "revocation_pending", remoteRevoked: true });
|
|
1056
|
+
}
|
|
1057
|
+
return Object.freeze({ localState: "deleted", remoteRevoked: true });
|
|
1058
|
+
}
|
|
1059
|
+
await this.saveIfCurrent(principalId, slot, generation, pending, cleanup.signal);
|
|
1060
|
+
return Object.freeze({ localState: "revocation_pending", remoteRevoked: false });
|
|
1061
|
+
} finally {
|
|
1062
|
+
completion.resolve();
|
|
1063
|
+
await slot.gate.run(() => {
|
|
1064
|
+
if (slot.disconnect === completion.promise) delete slot.disconnect;
|
|
1065
|
+
});
|
|
1066
|
+
await this.releaseSlot(principalId, slot);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
requiredClientId() {
|
|
1070
|
+
if (this.options.clientId === void 0 || this.options.clientId.length === 0) {
|
|
1071
|
+
throw authRequired("gmail_grant_missing");
|
|
1072
|
+
}
|
|
1073
|
+
return this.options.clientId;
|
|
1074
|
+
}
|
|
1075
|
+
checkDeadline(deadline) {
|
|
1076
|
+
if (deadline !== void 0 && deadline <= this.dependencies.clock.nowMonotonic()) {
|
|
1077
|
+
throw timeoutError();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
async runConnect(principalId, slot, generation, clientId, signal) {
|
|
1081
|
+
const callbackDeadline = this.dependencies.clock.nowMonotonic() + CALLBACK_MS;
|
|
1082
|
+
const callback = await this.dependencies.callbackFactory.open(signal, callbackDeadline);
|
|
1083
|
+
try {
|
|
1084
|
+
const state = base64Url(this.dependencies.randomBytes(32));
|
|
1085
|
+
const verifier = base64Url(this.dependencies.randomBytes(64));
|
|
1086
|
+
if (verifier.length < 43 || verifier.length > 128) throw new OAuthOperationError();
|
|
1087
|
+
const challenge = crypto.createHash("sha256").update(verifier, "ascii").digest("base64url");
|
|
1088
|
+
const authorization = new URL(GOOGLE_AUTH_URL);
|
|
1089
|
+
authorization.search = new URLSearchParams({
|
|
1090
|
+
client_id: clientId,
|
|
1091
|
+
redirect_uri: callback.redirectUri,
|
|
1092
|
+
response_type: "code",
|
|
1093
|
+
scope: this.options.scopes.join(" "),
|
|
1094
|
+
state,
|
|
1095
|
+
access_type: "offline",
|
|
1096
|
+
prompt: "consent",
|
|
1097
|
+
code_challenge: challenge,
|
|
1098
|
+
code_challenge_method: "S256"
|
|
1099
|
+
}).toString();
|
|
1100
|
+
await this.dependencies.browser.open(authorization.href, signal, callbackDeadline);
|
|
1101
|
+
const code = boundedString(await callback.waitForCode(state, signal, callbackDeadline));
|
|
1102
|
+
const form = {
|
|
1103
|
+
code,
|
|
1104
|
+
code_verifier: verifier,
|
|
1105
|
+
client_id: clientId,
|
|
1106
|
+
redirect_uri: callback.redirectUri,
|
|
1107
|
+
grant_type: "authorization_code"
|
|
1108
|
+
};
|
|
1109
|
+
if (this.options.clientSecret !== void 0) form.client_secret = this.options.clientSecret;
|
|
1110
|
+
const response = await this.dependencies.http.postForm(
|
|
1111
|
+
GOOGLE_TOKEN_URL,
|
|
1112
|
+
form,
|
|
1113
|
+
signal,
|
|
1114
|
+
this.dependencies.clock.nowMonotonic() + OPERATION_MS
|
|
1115
|
+
);
|
|
1116
|
+
if (response.status !== 200) throw new OAuthOperationError();
|
|
1117
|
+
const tokens = this.tokensFromResponse(response.bytes, void 0, true);
|
|
1118
|
+
await this.saveIfCurrent(
|
|
1119
|
+
principalId,
|
|
1120
|
+
slot,
|
|
1121
|
+
generation,
|
|
1122
|
+
snapshotOAuthCredentialRecord({ schemaVersion: 1, state: "active", tokens }),
|
|
1123
|
+
signal,
|
|
1124
|
+
true
|
|
1125
|
+
);
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
if (error instanceof DOMException || error instanceof IntegrationExecutionError) throw error;
|
|
1128
|
+
throw new OAuthOperationError();
|
|
1129
|
+
} finally {
|
|
1130
|
+
await callback.close();
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
async joinRefresh(principalId, slot, generation, record, context) {
|
|
1134
|
+
let flight;
|
|
1135
|
+
await slot.gate.run(() => {
|
|
1136
|
+
if (slot.blocked || slot.generation !== generation) throw authRequired("gmail_grant_missing");
|
|
1137
|
+
if (slot.refresh === void 0 || slot.refresh.generation !== generation) {
|
|
1138
|
+
const controller = new AbortController();
|
|
1139
|
+
const identity = {};
|
|
1140
|
+
const deadline = this.dependencies.clock.nowMonotonic() + OPERATION_MS;
|
|
1141
|
+
const promise = this.runRefreshWithDeadline(
|
|
1142
|
+
() => this.runRefresh(principalId, slot, generation, record, controller.signal, deadline),
|
|
1143
|
+
controller
|
|
1144
|
+
).finally(async () => {
|
|
1145
|
+
await slot.gate.run(() => {
|
|
1146
|
+
if (slot.refresh?.identity === identity) delete slot.refresh;
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
slot.refresh = { identity, generation, controller, promise, waiters: 0 };
|
|
1150
|
+
}
|
|
1151
|
+
flight = slot.refresh;
|
|
1152
|
+
flight.waiters += 1;
|
|
1153
|
+
});
|
|
1154
|
+
try {
|
|
1155
|
+
return await raceSignal(
|
|
1156
|
+
flight.promise,
|
|
1157
|
+
context.signal,
|
|
1158
|
+
context.deadlineMonotonicMs,
|
|
1159
|
+
() => this.dependencies.clock.nowMonotonic()
|
|
1160
|
+
);
|
|
1161
|
+
} finally {
|
|
1162
|
+
let abort = false;
|
|
1163
|
+
await slot.gate.run(() => {
|
|
1164
|
+
flight.waiters -= 1;
|
|
1165
|
+
abort = flight.waiters === 0 && slot.refresh?.identity === flight.identity;
|
|
1166
|
+
});
|
|
1167
|
+
if (abort) {
|
|
1168
|
+
flight.controller.abort(abortError());
|
|
1169
|
+
await Promise.allSettled([flight.promise]);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
async runRefresh(principalId, slot, generation, record, signal, deadlineMonotonicMs) {
|
|
1174
|
+
const form = {
|
|
1175
|
+
refresh_token: record.tokens.refreshToken,
|
|
1176
|
+
client_id: this.requiredClientId(),
|
|
1177
|
+
grant_type: "refresh_token"
|
|
1178
|
+
};
|
|
1179
|
+
if (this.options.clientSecret !== void 0) form.client_secret = this.options.clientSecret;
|
|
1180
|
+
const response = await this.dependencies.http.postForm(
|
|
1181
|
+
GOOGLE_TOKEN_URL,
|
|
1182
|
+
form,
|
|
1183
|
+
signal,
|
|
1184
|
+
deadlineMonotonicMs
|
|
1185
|
+
);
|
|
1186
|
+
if (response.status !== 200) {
|
|
1187
|
+
if (providerErrorCode(response.bytes) === "invalid_grant") {
|
|
1188
|
+
await this.deleteIfCurrent(principalId, slot, generation, signal);
|
|
1189
|
+
throw authRequired("gmail_grant_missing");
|
|
1190
|
+
}
|
|
1191
|
+
throw new IntegrationExecutionError("api_rejected");
|
|
1192
|
+
}
|
|
1193
|
+
let tokens;
|
|
1194
|
+
try {
|
|
1195
|
+
tokens = this.tokensFromResponse(response.bytes, record.tokens, false);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
if (!(error instanceof OAuthScopeDriftError)) throw error;
|
|
1198
|
+
await this.deleteIfCurrent(principalId, slot, generation, signal);
|
|
1199
|
+
throw authRequired("gmail_scope_drift");
|
|
1200
|
+
}
|
|
1201
|
+
await this.saveIfCurrent(
|
|
1202
|
+
principalId,
|
|
1203
|
+
slot,
|
|
1204
|
+
generation,
|
|
1205
|
+
snapshotOAuthCredentialRecord({ schemaVersion: 1, state: "active", tokens }),
|
|
1206
|
+
signal
|
|
1207
|
+
);
|
|
1208
|
+
return tokens.accessToken;
|
|
1209
|
+
}
|
|
1210
|
+
async runRefreshWithDeadline(start, controller) {
|
|
1211
|
+
const operation = start();
|
|
1212
|
+
let timedOut = false;
|
|
1213
|
+
let timer;
|
|
1214
|
+
const timeout = new Promise((_, reject) => {
|
|
1215
|
+
timer = setTimeout(() => {
|
|
1216
|
+
timedOut = true;
|
|
1217
|
+
controller.abort(abortError());
|
|
1218
|
+
reject(timeoutError());
|
|
1219
|
+
}, OPERATION_MS);
|
|
1220
|
+
});
|
|
1221
|
+
try {
|
|
1222
|
+
return await Promise.race([operation, timeout]);
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
if (!timedOut) throw error;
|
|
1225
|
+
await Promise.allSettled([operation]);
|
|
1226
|
+
throw timeoutError();
|
|
1227
|
+
} finally {
|
|
1228
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
tokensFromResponse(bytes, fallback, requireScope) {
|
|
1232
|
+
const value = parseProviderJson(bytes);
|
|
1233
|
+
if (value.token_type !== "Bearer") throw new OAuthOperationError();
|
|
1234
|
+
const expires = value.expires_in;
|
|
1235
|
+
if (typeof expires !== "number" || !Number.isFinite(expires) || expires <= 0 || expires > 604800) {
|
|
1236
|
+
throw new OAuthOperationError();
|
|
1237
|
+
}
|
|
1238
|
+
const refreshToken = value.refresh_token === void 0 && fallback !== void 0 ? fallback.refreshToken : boundedString(value.refresh_token);
|
|
1239
|
+
let grantedScopes;
|
|
1240
|
+
if (value.scope === void 0) {
|
|
1241
|
+
if (requireScope || fallback === void 0) throw new OAuthOperationError();
|
|
1242
|
+
grantedScopes = fallback.grantedScopes;
|
|
1243
|
+
} else {
|
|
1244
|
+
if (typeof value.scope !== "string") throw new OAuthOperationError();
|
|
1245
|
+
grantedScopes = normalizedScopes(value.scope.split(/\s+/).filter(Boolean), true);
|
|
1246
|
+
if (!isSuperset(grantedScopes, this.options.scopes)) {
|
|
1247
|
+
if (fallback !== void 0) throw new OAuthScopeDriftError();
|
|
1248
|
+
throw new OAuthOperationError();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return Object.freeze({
|
|
1252
|
+
accessToken: boundedString(value.access_token),
|
|
1253
|
+
refreshToken,
|
|
1254
|
+
expiresAtEpochMs: Math.floor(
|
|
1255
|
+
this.dependencies.clock.nowWallSeconds() * 1e3 + expires * 1e3
|
|
1256
|
+
),
|
|
1257
|
+
grantedScopes,
|
|
1258
|
+
tokenType: "Bearer"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
async saveIfCurrent(principalId, slot, generation, record, signal, allowBlockedActive = false) {
|
|
1262
|
+
await slot.gate.run(() => {
|
|
1263
|
+
if (slot.generation !== generation || slot.blocked && record.state === "active" && !allowBlockedActive) {
|
|
1264
|
+
throw authRequired("gmail_grant_missing");
|
|
1265
|
+
}
|
|
1266
|
+
return this.options.storage.save(principalId, record, signal);
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
async deleteIfCurrent(principalId, slot, generation, signal) {
|
|
1270
|
+
await slot.gate.run(() => {
|
|
1271
|
+
if (slot.generation !== generation || slot.blocked) throw authRequired("gmail_grant_missing");
|
|
1272
|
+
return this.options.storage.delete(principalId, signal);
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
async deleteBlockedIfCurrent(principalId, slot, generation, signal) {
|
|
1276
|
+
await slot.gate.run(() => {
|
|
1277
|
+
if (slot.generation !== generation || !slot.blocked) {
|
|
1278
|
+
throw authRequired("gmail_grant_missing");
|
|
1279
|
+
}
|
|
1280
|
+
return this.options.storage.delete(principalId, signal);
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
async acquireSlot(principalId) {
|
|
1284
|
+
return this.slotsGate.run(() => {
|
|
1285
|
+
let slot = this.slots.get(principalId);
|
|
1286
|
+
if (slot === void 0) {
|
|
1287
|
+
slot = { gate: new Mutex(), generation: 0, blocked: false, references: 0 };
|
|
1288
|
+
this.slots.set(principalId, slot);
|
|
1289
|
+
}
|
|
1290
|
+
slot.references += 1;
|
|
1291
|
+
return slot;
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
async releaseSlot(principalId, slot) {
|
|
1295
|
+
await this.slotsGate.run(() => {
|
|
1296
|
+
slot.references -= 1;
|
|
1297
|
+
if (slot.references === 0 && slot.connect === void 0 && slot.refresh === void 0 && slot.disconnect === void 0 && this.slots.get(principalId) === slot) {
|
|
1298
|
+
this.slots.delete(principalId);
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
function isSuperset(candidate, required) {
|
|
1304
|
+
const values = new Set(candidate);
|
|
1305
|
+
return required.every((scope) => values.has(scope));
|
|
1306
|
+
}
|
|
1307
|
+
function validatedOptions(options) {
|
|
1308
|
+
if (options === null || typeof options !== "object" || typeof options.storage !== "object") {
|
|
1309
|
+
throw policyError();
|
|
1310
|
+
}
|
|
1311
|
+
const scopes = normalizedScopes(options.scopes);
|
|
1312
|
+
const clientId = options.clientId === void 0 || options.clientId === "" ? void 0 : boundedString(options.clientId, 4096);
|
|
1313
|
+
const clientSecret = options.clientSecret === void 0 ? void 0 : boundedString(options.clientSecret);
|
|
1314
|
+
return Object.freeze({
|
|
1315
|
+
...clientId === void 0 ? {} : { clientId },
|
|
1316
|
+
...clientSecret === void 0 ? {} : { clientSecret },
|
|
1317
|
+
scopes,
|
|
1318
|
+
storage: options.storage
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
var SECURITY = "/usr/bin/security";
|
|
1322
|
+
var INTEGRATION_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
1323
|
+
var MAX_INTEGRATION_NAME_LENGTH = 128;
|
|
1324
|
+
var TIMEOUT_MS = 1e4;
|
|
1325
|
+
var MAX_STDOUT_BYTES = 16 * 1024 + 1;
|
|
1326
|
+
var MAX_STDERR_BYTES = 8 * 1024;
|
|
1327
|
+
var TERM_GRACE_MS = 250;
|
|
1328
|
+
var ProcessFailure = class extends Error {
|
|
1329
|
+
constructor(kind) {
|
|
1330
|
+
super("Keychain process failed");
|
|
1331
|
+
this.kind = kind;
|
|
1332
|
+
}
|
|
1333
|
+
kind;
|
|
1334
|
+
};
|
|
1335
|
+
var SpawnKeychainProcess = class {
|
|
1336
|
+
constructor(spawnChild = child_process.spawn) {
|
|
1337
|
+
this.spawnChild = spawnChild;
|
|
1338
|
+
}
|
|
1339
|
+
spawnChild;
|
|
1340
|
+
run(args, options) {
|
|
1341
|
+
if (options.signal.aborted) return Promise.reject(new ProcessFailure("cancelled"));
|
|
1342
|
+
return new Promise((resolve, reject) => {
|
|
1343
|
+
let child;
|
|
1344
|
+
try {
|
|
1345
|
+
child = this.spawnChild(SECURITY, args, {
|
|
1346
|
+
shell: false,
|
|
1347
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1348
|
+
});
|
|
1349
|
+
} catch {
|
|
1350
|
+
reject(new ProcessFailure("failed"));
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
const stdout = [];
|
|
1354
|
+
let stdoutBytes = 0;
|
|
1355
|
+
let stderrBytes = 0;
|
|
1356
|
+
let failure;
|
|
1357
|
+
let grace;
|
|
1358
|
+
const terminate = (next) => {
|
|
1359
|
+
if (failure === void 0) failure = next;
|
|
1360
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
1361
|
+
child.kill("SIGTERM");
|
|
1362
|
+
grace ??= setTimeout(() => {
|
|
1363
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
1364
|
+
}, TERM_GRACE_MS);
|
|
1365
|
+
};
|
|
1366
|
+
child.stdout.on("data", (chunk) => {
|
|
1367
|
+
stdoutBytes += chunk.byteLength;
|
|
1368
|
+
if (stdoutBytes > options.maxStdoutBytes) terminate(new ProcessFailure("overflow"));
|
|
1369
|
+
else stdout.push(chunk);
|
|
1370
|
+
});
|
|
1371
|
+
child.stderr.on("data", (chunk) => {
|
|
1372
|
+
stderrBytes += chunk.byteLength;
|
|
1373
|
+
if (stderrBytes > MAX_STDERR_BYTES) terminate(new ProcessFailure("overflow"));
|
|
1374
|
+
});
|
|
1375
|
+
const onAbort = () => terminate(new ProcessFailure("cancelled"));
|
|
1376
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1377
|
+
const timeout = setTimeout(() => terminate(new ProcessFailure("timeout")), options.timeoutMs);
|
|
1378
|
+
child.once("error", () => terminate(new ProcessFailure("failed")));
|
|
1379
|
+
child.once("close", (code) => {
|
|
1380
|
+
clearTimeout(timeout);
|
|
1381
|
+
if (grace !== void 0) clearTimeout(grace);
|
|
1382
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
1383
|
+
if (failure !== void 0) {
|
|
1384
|
+
reject(failure);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
resolve({
|
|
1389
|
+
code: code ?? 1,
|
|
1390
|
+
stdout: new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(stdout))
|
|
1391
|
+
});
|
|
1392
|
+
} catch {
|
|
1393
|
+
reject(new ProcessFailure("overflow"));
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
child.stdin.once("error", () => terminate(new ProcessFailure("failed")));
|
|
1397
|
+
child.stdin.end(options.stdin);
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
function executable() {
|
|
1402
|
+
try {
|
|
1403
|
+
fs.accessSync(SECURITY, fs.constants.X_OK);
|
|
1404
|
+
return true;
|
|
1405
|
+
} catch {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
function serviceFor(integrationName) {
|
|
1410
|
+
if (typeof integrationName !== "string" || integrationName.length > MAX_INTEGRATION_NAME_LENGTH || !INTEGRATION_NAME.test(integrationName)) {
|
|
1411
|
+
throw new IntegrationPolicyError();
|
|
1412
|
+
}
|
|
1413
|
+
return `dev.kaji.oauth.${integrationName}`;
|
|
1414
|
+
}
|
|
1415
|
+
function accountFor(service, principalId) {
|
|
1416
|
+
principalId = validateOAuthPrincipal(principalId);
|
|
1417
|
+
return crypto.createHash("sha256").update(`${service}\0${principalId}`, "utf8").digest("hex");
|
|
1418
|
+
}
|
|
1419
|
+
var MacOSKeychainTokenStorage = class _MacOSKeychainTokenStorage {
|
|
1420
|
+
process;
|
|
1421
|
+
platform;
|
|
1422
|
+
executable;
|
|
1423
|
+
service;
|
|
1424
|
+
constructor(integrationName = "gmail") {
|
|
1425
|
+
this.service = serviceFor(integrationName);
|
|
1426
|
+
this.process = new SpawnKeychainProcess();
|
|
1427
|
+
this.platform = os.platform();
|
|
1428
|
+
this.executable = executable();
|
|
1429
|
+
}
|
|
1430
|
+
/** @internal Source-relative tests only. */
|
|
1431
|
+
static _create(options) {
|
|
1432
|
+
const service = serviceFor(options.integrationName ?? "gmail");
|
|
1433
|
+
const storage = Object.create(_MacOSKeychainTokenStorage.prototype);
|
|
1434
|
+
Object.defineProperties(storage, {
|
|
1435
|
+
process: { value: options.process },
|
|
1436
|
+
platform: { value: options.platform },
|
|
1437
|
+
executable: { value: options.executable },
|
|
1438
|
+
service: { value: service }
|
|
1439
|
+
});
|
|
1440
|
+
return storage;
|
|
1441
|
+
}
|
|
1442
|
+
async load(principalId, signal) {
|
|
1443
|
+
const account = this.preflight(principalId);
|
|
1444
|
+
const result = await this.run(
|
|
1445
|
+
["find-generic-password", "-a", account, "-s", this.service, "-w"],
|
|
1446
|
+
{ signal }
|
|
1447
|
+
);
|
|
1448
|
+
if (result.code === 44) return void 0;
|
|
1449
|
+
if (result.code !== 0) throw new IntegrationAuthError("keychain_locked");
|
|
1450
|
+
const output = result.stdout.endsWith("\n") ? result.stdout.slice(0, -1) : result.stdout;
|
|
1451
|
+
try {
|
|
1452
|
+
if (new TextEncoder().encode(output).byteLength > 16 * 1024) throw new Error();
|
|
1453
|
+
return snapshotOAuthCredentialRecord(JSON.parse(output));
|
|
1454
|
+
} catch {
|
|
1455
|
+
throw new IntegrationAuthError("keychain_corrupt");
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
async save(principalId, record, signal) {
|
|
1459
|
+
const account = this.preflight(principalId);
|
|
1460
|
+
const stdin = canonicalOAuthCredentialJson(record);
|
|
1461
|
+
const result = await this.run(
|
|
1462
|
+
["add-generic-password", "-a", account, "-s", this.service, "-U", "-w"],
|
|
1463
|
+
{ signal, stdin }
|
|
1464
|
+
);
|
|
1465
|
+
if (result.code !== 0) throw new IntegrationAuthError("keychain_locked");
|
|
1466
|
+
}
|
|
1467
|
+
async delete(principalId, signal) {
|
|
1468
|
+
const account = this.preflight(principalId);
|
|
1469
|
+
const result = await this.run(["delete-generic-password", "-a", account, "-s", this.service], {
|
|
1470
|
+
signal
|
|
1471
|
+
});
|
|
1472
|
+
if (result.code !== 0 && result.code !== 44) {
|
|
1473
|
+
throw new IntegrationAuthError("keychain_locked");
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
preflight(principalId) {
|
|
1477
|
+
const account = accountFor(this.service, principalId);
|
|
1478
|
+
if (this.platform !== "darwin" || !this.executable) {
|
|
1479
|
+
throw new IntegrationAuthError("keychain_unsupported");
|
|
1480
|
+
}
|
|
1481
|
+
return account;
|
|
1482
|
+
}
|
|
1483
|
+
async run(args, options) {
|
|
1484
|
+
try {
|
|
1485
|
+
return await this.process.run(args, {
|
|
1486
|
+
...options,
|
|
1487
|
+
timeoutMs: TIMEOUT_MS,
|
|
1488
|
+
maxStdoutBytes: MAX_STDOUT_BYTES
|
|
1489
|
+
});
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
if (error instanceof ProcessFailure && error.kind === "cancelled") {
|
|
1492
|
+
throw new DOMException("Keychain operation cancelled", "AbortError");
|
|
1493
|
+
}
|
|
1494
|
+
if (error instanceof ProcessFailure && error.kind === "overflow") {
|
|
1495
|
+
throw new IntegrationAuthError("keychain_corrupt");
|
|
1496
|
+
}
|
|
1497
|
+
throw new IntegrationAuthError("keychain_locked");
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
exports.GoogleOAuthClient = GoogleOAuthClient;
|
|
1503
|
+
exports.MacOSKeychainTokenStorage = MacOSKeychainTokenStorage;
|
|
1504
|
+
exports.canonicalOAuthCredentialJson = canonicalOAuthCredentialJson;
|
|
1505
|
+
exports.snapshotOAuthCredentialRecord = snapshotOAuthCredentialRecord;
|
|
1506
|
+
//# sourceMappingURL=auth.cjs.map
|
|
1507
|
+
//# sourceMappingURL=auth.cjs.map
|