@desplega.ai/agent-swarm 1.119.6 → 1.121.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{actions-qe5zjcbr.js → actions-mwshtkf7.js} +6 -6
- package/dist/{anthropic-messages-qjxzsabf.js → anthropic-messages-4gn944cn.js} +29 -13
- package/dist/{app-31x7k3vj.js → app-skbty91x.js} +4 -4
- package/dist/{assistant-4crt9dve.js → assistant-tg7dhfj8.js} +13 -13
- package/dist/{azure-openai-responses-ntfd05zd.js → azure-openai-responses-79zc0wyy.js} +28 -13
- package/dist/{boot-reembed-8g69ttz4.js → boot-reembed-ntjmeyh2.js} +4 -4
- package/dist/{boot-reembed-q648j733.js → boot-reembed-q5x0xpcp.js} +5 -5
- package/dist/{boot-scrub-logs-sfmzd2hj.js → boot-scrub-logs-5gznsfbe.js} +4 -4
- package/dist/{claude-adapter-9n3nzt65.js → claude-adapter-vxvcym3r.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-g807894s.js} +2 -2
- package/dist/{cli-bpvgs2y6.js → cli-072t49hf.js} +2123 -1533
- package/dist/{cli-g6qz2kts.js → cli-0df2g86x.js} +336 -18
- package/dist/cli-1728qh70.js +791 -0
- package/dist/{cli-vg1jxfqz.js → cli-3b1ayz83.js} +5 -5
- package/dist/{cli-r93nz80j.js → cli-3fxqwfh3.js} +1 -1
- package/dist/{cli-w6rhc30s.js → cli-3jrdwnn9.js} +20 -5
- package/dist/{cli-pasyk3vc.js → cli-46etyprn.js} +10 -5
- package/dist/{cli-433c0f4b.js → cli-4pfweq4x.js} +8 -8
- package/dist/{cli-2r7s8jhp.js → cli-5c4327dx.js} +7 -1
- package/dist/{cli-4s84mw72.js → cli-5hpvd15x.js} +2 -2
- package/dist/{cli-branks1w.js → cli-6s3ytc5x.js} +1 -1
- package/dist/{cli-da3x3btq.js → cli-7pt9sc83.js} +2 -2
- package/dist/{cli-2gv8ygwr.js → cli-7pzae66m.js} +2 -2
- package/dist/{cli-qs7nb3zn.js → cli-8wrympd7.js} +27 -1
- package/dist/{cli-qw1ccvxm.js → cli-95rbe11h.js} +70 -51
- package/dist/{cli-6rvm3jkk.js → cli-agdrxyex.js} +1 -1
- package/dist/{cli-wr706gez.js → cli-av2s7my4.js} +167 -66
- package/dist/{cli-59jy217p.js → cli-avw9ev3x.js} +55 -21
- package/dist/{cli-fkxxh5d4.js → cli-b2mqkg1h.js} +2 -2
- package/dist/{cli-1arw00dp.js → cli-ccjwgarc.js} +12 -3
- package/dist/{cli-pscrc6tw.js → cli-ch7t5v7n.js} +5 -0
- package/dist/{cli-tghre7g6.js → cli-cw70hymf.js} +1 -0
- package/dist/{cli-n61x2aph.js → cli-dmzv0986.js} +816 -1335
- package/dist/{cli-d1reabed.js → cli-g9vva2qh.js} +2 -2
- package/dist/{cli-ner6af7m.js → cli-gdpx70nc.js} +24048 -23715
- package/dist/{cli-96feg24n.js → cli-ghq3v4c9.js} +2 -2
- package/dist/{cli-wxbnjgce.js → cli-ky8esb8t.js} +3 -3
- package/dist/{cli-gdqdkr3j.js → cli-nf7sadev.js} +2 -2
- package/dist/{cli-h6cf69x5.js → cli-p9w7kb3h.js} +1 -1
- package/dist/{cli-xeatwfbe.js → cli-ry656xem.js} +24 -17
- package/dist/{cli-q8dway4w.js → cli-s8bdcke2.js} +1 -1
- package/dist/cli-s8s499xy.js +81 -0
- package/dist/{cli-sbkwvn5e.js → cli-srjqn7fd.js} +1 -1
- package/dist/cli-tv7vekye.js +367 -0
- package/dist/{cli-gg5bd32n.js → cli-vg3zrw6h.js} +4 -4
- package/dist/{cli-vx0y68jv.js → cli-vp9r4ap4.js} +5 -4
- package/dist/{cli-mg280wsv.js → cli-we156bmz.js} +1 -1
- package/dist/{cli-86fdnerz.js → cli-wwsaj97q.js} +9 -4
- package/dist/{cli-e0y603nn.js → cli-x0e0yvnq.js} +101 -1
- package/dist/{cli-mw3xpgwb.js → cli-x4apc9t3.js} +4 -4
- package/dist/cli.js +26 -21
- package/dist/{codex-adapter-6j2b38tg.js → codex-adapter-mmy741zd.js} +8 -9
- package/dist/{codex-session-runner-vz215wgw.js → codex-session-runner-sazmx9zt.js} +8 -9
- package/dist/{commands-6xr20jbe.js → commands-cxxye5e4.js} +3 -3
- package/dist/{db-1mmvk1z4.js → db-ab1954b7.js} +5 -3
- package/dist/{google-generative-ai-n1d3xt4n.js → google-generative-ai-wwa2ked5.js} +14 -18
- package/dist/{google-vertex-mxst23gy.js → google-vertex-71xmdd7n.js} +14 -18
- package/dist/{handlers-xvrknk8w.js → handlers-fmfe2343.js} +13 -13
- package/dist/{hook-qm7he8wn.js → hook-ygnw3cvt.js} +7 -8
- package/dist/{http-hj08zq67.js → http-kactcc0z.js} +1128 -536
- package/dist/{index-688jph6a.js → index-6erq0wvk.js} +11 -11
- package/dist/{index-gze4fg2w.js → index-g86j2yt5.js} +9 -9
- package/dist/{index-etz7d8md.js → index-gmzs6a2q.js} +10 -10
- package/dist/{index-wj8dfpwh.js → index-hmp82wn0.js} +12 -12
- package/dist/{keepalive-94wn9yxw.js → keepalive-3cvt26dw.js} +18 -14
- package/dist/{lead-rvcqkp11.js → lead-ysf26cjr.js} +31 -32
- package/dist/{maintenance-qhfc4dnp.js → maintenance-9npwptbb.js} +5 -5
- package/dist/{mistral-conversations-ykcg66za.js → mistral-conversations-26ya4yah.js} +17 -13
- package/dist/{oauth-refresh-sweep-rj8wgvsw.js → oauth-refresh-sweep-0r2v76vw.js} +23 -17
- package/dist/{onboard-8bv8q1wc.js → onboard-ddb1674a.js} +2 -2
- package/dist/{openai-codex-responses-zy0907j9.js → openai-codex-responses-6rr2ye0s.js} +78 -38
- package/dist/{openai-completions-xk4p56n9.js → openai-completions-zfa9nkvt.js} +130 -33
- package/dist/{openai-responses-4gv3ca5d.js → openai-responses-9wmdks0g.js} +38 -14
- package/dist/{opencode-adapter-5bwzvbwj.js → opencode-adapter-h6rkxwrk.js} +2 -2
- package/dist/{openrouter-images-4fsrrzes.js → openrouter-images-dgq98729.js} +10 -3
- package/dist/{otel-impl-1kf6fr2m.js → otel-impl-b43bkq8e.js} +4 -4
- package/dist/{pi-messages-zb8v76wx.js → pi-messages-nzjbbz47.js} +4 -6
- package/dist/{pi-mono-adapter-9sg69kg3.js → pi-mono-adapter-ke789738.js} +2328 -1281
- package/dist/{pricing-refresh-e84tar2s.js → pricing-refresh-2rqf114z.js} +5 -5
- package/dist/{rbac-roles-atytq5ch.js → rbac-roles-5wkhc8xv.js} +5 -5
- package/dist/{rbac-roles-pg0zvnwn.js → rbac-roles-6vtafvzb.js} +4 -4
- package/dist/{seed-pricing-xd6nxq7k.js → seed-pricing-23wrb873.js} +4 -4
- package/dist/{setup-4z389yk0.js → setup-6rny4fk6.js} +2 -2
- package/dist/{worker-m61r0t7m.js → worker-yapewe14.js} +31 -32
- package/openapi.json +2671 -1811
- package/package.json +10 -5
- package/plugin/commands/swarm-chat.md +2 -0
- package/plugin/pi-skills/swarm-chat/SKILL.md +2 -0
- package/src/be/connection-bindings-blob-migration.ts +123 -0
- package/src/be/db-queries/mcp-oauth.ts +409 -247
- package/src/be/db-queries/oauth.ts +813 -131
- package/src/be/db.ts +97 -6
- package/src/be/migrations/117_unified_oauth.sql +624 -0
- package/src/be/migrations/119_agent_avatar.sql +7 -0
- package/src/be/migrations/120_task_title.sql +3 -0
- package/src/be/oauth-credential-bindings.ts +48 -14
- package/src/be/oauth-encryption-backfill.ts +84 -0
- package/src/be/oauth-refresh-sweep.ts +38 -23
- package/src/be/script-connections.ts +1130 -163
- package/src/be/script-credential-broker.ts +37 -27
- package/src/be/scripts/typecheck.ts +3 -2
- package/src/be/seed-capabilities.ts +62 -0
- package/src/be/seed-pricing.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/vendored-openapi.ts +98 -0
- package/src/commands/runner.ts +69 -6
- package/src/http/agents.ts +23 -5
- package/src/http/all-routes.ts +1 -0
- package/src/http/core.ts +8 -5
- package/src/http/index.ts +18 -5
- package/src/http/mcp-bridge.ts +5 -2
- package/src/http/mcp-oauth.ts +103 -101
- package/src/http/oauth-callback.ts +332 -0
- package/src/http/oauth-generic.ts +14 -54
- package/src/http/poll.ts +5 -1
- package/src/http/script-connections.ts +651 -170
- package/src/http/scripts.ts +4 -2
- package/src/http/sessions.ts +1 -1
- package/src/http/tasks.ts +35 -0
- package/src/http/trackers/jira.ts +22 -34
- package/src/http/trackers/linear.ts +21 -36
- package/src/http/x.ts +6 -2
- package/src/jira/app.ts +12 -6
- package/src/jira/oauth.ts +48 -48
- package/src/linear/app.ts +44 -2
- package/src/linear/oauth.ts +12 -37
- package/src/oauth/app-validation.ts +27 -9
- package/src/oauth/ensure-mcp-token.ts +1 -0
- package/src/oauth/ensure-token.ts +331 -108
- package/src/oauth/identity-capture.ts +103 -0
- package/src/oauth/keepalive.ts +14 -9
- package/src/oauth/presets.ts +242 -0
- package/src/oauth/wrapper.ts +197 -76
- package/src/prompts/base-prompt.ts +48 -4
- package/src/prompts/session-templates.ts +13 -1
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/rbac/legacy-policy.ts +2 -1
- package/src/rbac/permissions.ts +7 -3
- package/src/scripts-runtime/credential-broker/broker.ts +51 -8
- package/src/scripts-runtime/credential-broker/fetch-patch.ts +30 -3
- package/src/scripts-runtime/credential-broker/index.ts +1 -1
- package/src/scripts-runtime/credential-broker/types.ts +55 -8
- package/src/scripts-runtime/egress-secrets.ts +6 -2
- package/src/scripts-runtime/eval-harness.ts +2 -2
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -0
- package/src/scripts-runtime/types/stdlib.d.ts +11 -0
- package/src/server.ts +230 -126
- package/src/tests/agent-avatar-resolution.test.ts +123 -0
- package/src/tests/agents-list-model-display.test.ts +10 -0
- package/src/tests/base-prompt.test.ts +72 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/connection-embedded-auth.test.ts +709 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/credential-broker.test.ts +123 -125
- package/src/tests/db-queries-oauth.test.ts +126 -1
- package/src/tests/ensure-token.test.ts +44 -2
- package/src/tests/list-endpoint-slimming.test.ts +45 -0
- package/src/tests/mcp-oauth-manual-client.test.ts +20 -1
- package/src/tests/mcp-oauth-queries.test.ts +154 -8
- package/src/tests/oauth-access-token-tool.test.ts +37 -1
- package/src/tests/oauth-callback-flow.test.ts +534 -0
- package/src/tests/oauth-credential-bindings.test.ts +50 -22
- package/src/tests/oauth-presets.test.ts +275 -0
- package/src/tests/oauth-refresh-failure.test.ts +263 -0
- package/src/tests/oauth-refresh-sweep.test.ts +83 -3
- package/src/tests/oauth-wrapper.test.ts +77 -55
- package/src/tests/prompt-template-session.test.ts +6 -5
- package/src/tests/rbac-charact-misc-tools.test.ts +60 -0
- package/src/tests/rbac-engine.test.ts +2 -1
- package/src/tests/script-connections-http.test.ts +356 -11
- package/src/tests/script-connections.test.ts +102 -0
- package/src/tests/sdk-allowlist.test.ts +4 -1
- package/src/tests/seed-capabilities.test.ts +92 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/sessions.test.ts +46 -0
- package/src/tests/spec-base-url.test.ts +266 -0
- package/src/tests/status.test.ts +3 -4
- package/src/tests/swarm-config-reserved-keys.test.ts +4 -2
- package/src/tests/task-title-route.test.ts +106 -0
- package/src/tests/tool-annotations.test.ts +6 -3
- package/src/tests/tracker-fold.test.ts +412 -0
- package/src/tests/unified-oauth-migration.test.ts +455 -0
- package/src/tests/vendored-openapi.test.ts +263 -0
- package/src/tools/credential-bindings/tool.ts +232 -77
- package/src/tools/oauth-access-token.ts +55 -6
- package/src/tools/script-connections/tool.ts +73 -53
- package/src/tools/swarm-config/set-config.ts +24 -50
- package/src/tools/update-profile.ts +15 -5
- package/src/tracker/types.ts +12 -0
- package/src/types.ts +29 -0
- package/src/utils/context-window.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +6 -2
- package/vendored-openapi/README.md +22 -0
- package/vendored-openapi/github.json +7759 -0
- package/vendored-openapi/gmail.json +393 -0
- package/vendored-openapi/jira.json +3396 -0
- package/vendored-openapi/linear.json +252 -0
- package/vendored-openapi/manifest.json +128 -0
- package/vendored-openapi/slack.json +2895 -0
- package/dist/cli-0pygq9r1.js +0 -429
- package/dist/cli-8hhms4hp.js +0 -393
- package/dist/cli-z9gzq07x.js +0 -27
- package/src/scripts-runtime/credential-broker/store.ts +0 -72
- package/dist/{otel-g299k41b.js → otel-63jr5v38.js} +1 -1
|
@@ -1,107 +1,706 @@
|
|
|
1
1
|
import type { OAuthApp, OAuthTokens } from "../../tracker/types";
|
|
2
|
+
import { decryptSecret, encryptSecret, getEncryptionKey } from "../crypto";
|
|
2
3
|
import { normalizeDateRequired } from "../date-utils";
|
|
3
4
|
import { getDb } from "../db";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
type OAuthAppRow = Omit<
|
|
7
|
+
OAuthApp,
|
|
8
|
+
"clientSecretEncrypted" | "requiresRefreshTokenRotation" | "scopes"
|
|
9
|
+
> & {
|
|
10
|
+
clientSecret: string | null;
|
|
11
|
+
clientSecretEncrypted: number;
|
|
12
|
+
requiresRefreshTokenRotation: number;
|
|
13
|
+
scopes: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type OAuthAuthorizationStatus = "active" | "refresh-failed" | "expired" | "revoked";
|
|
17
|
+
|
|
18
|
+
type OAuthAuthorizationRow = {
|
|
19
|
+
id: string;
|
|
20
|
+
appId: string;
|
|
21
|
+
label: string;
|
|
22
|
+
userId: string | null;
|
|
23
|
+
accountEmail: string | null;
|
|
24
|
+
identityJson: string | null;
|
|
25
|
+
accessToken: string;
|
|
26
|
+
refreshToken: string | null;
|
|
27
|
+
tokenType: string;
|
|
28
|
+
expiresAt: string | null;
|
|
29
|
+
scope: string | null;
|
|
30
|
+
tokensEncrypted: number;
|
|
31
|
+
tokenVersion: number;
|
|
32
|
+
status: OAuthAuthorizationStatus;
|
|
33
|
+
lastErrorMessage: string | null;
|
|
34
|
+
lastRefreshedAt: string | null;
|
|
35
|
+
connectedByUserId: string | null;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
updatedAt: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type OAuthAuthorization = Omit<OAuthAuthorizationRow, "tokensEncrypted"> & {
|
|
41
|
+
tokensEncrypted: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function parseScopeList(value: string): string[] {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(value);
|
|
47
|
+
if (Array.isArray(parsed)) {
|
|
48
|
+
return parsed.filter((item): item is string => typeof item === "string");
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Legacy callers still pass comma-delimited scope strings.
|
|
52
|
+
}
|
|
53
|
+
return value
|
|
54
|
+
.split(",")
|
|
55
|
+
.map((scope) => scope.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function storeScopeList(value: string | string[]): string {
|
|
60
|
+
return JSON.stringify(Array.isArray(value) ? value : parseScopeList(value));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseObject(value: string | null | undefined): Record<string, unknown> {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(value || "{}");
|
|
66
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
67
|
+
? (parsed as Record<string, unknown>)
|
|
68
|
+
: {};
|
|
69
|
+
} catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function storageMetadata(metadata: string): {
|
|
75
|
+
metadata: string;
|
|
76
|
+
extraParamsJson: string | null;
|
|
77
|
+
tokenAuthStyle: "body" | "basic";
|
|
78
|
+
tokenBodyFormat: "form" | "json";
|
|
79
|
+
revocationUrl: string | null;
|
|
80
|
+
} {
|
|
81
|
+
const parsed = parseObject(metadata);
|
|
82
|
+
const extraParams =
|
|
83
|
+
parsed.extraParams &&
|
|
84
|
+
typeof parsed.extraParams === "object" &&
|
|
85
|
+
!Array.isArray(parsed.extraParams)
|
|
86
|
+
? parsed.extraParams
|
|
87
|
+
: null;
|
|
88
|
+
const tokenAuthStyle = parsed.tokenAuthStyle === "basic" ? "basic" : "body";
|
|
89
|
+
const tokenBodyFormat = parsed.tokenBodyFormat === "json" ? "json" : "form";
|
|
90
|
+
const revocationUrl = typeof parsed.revocationUrl === "string" ? parsed.revocationUrl : null;
|
|
91
|
+
const hasLiftedFields = [
|
|
92
|
+
"extraParams",
|
|
93
|
+
"tokenAuthStyle",
|
|
94
|
+
"tokenBodyFormat",
|
|
95
|
+
"revocationUrl",
|
|
96
|
+
].some((key) => Object.hasOwn(parsed, key));
|
|
97
|
+
if (hasLiftedFields) {
|
|
98
|
+
delete parsed.extraParams;
|
|
99
|
+
delete parsed.tokenAuthStyle;
|
|
100
|
+
delete parsed.tokenBodyFormat;
|
|
101
|
+
delete parsed.revocationUrl;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
metadata: hasLiftedFields ? JSON.stringify(parsed) : metadata,
|
|
105
|
+
extraParamsJson: extraParams ? JSON.stringify(extraParams) : null,
|
|
106
|
+
tokenAuthStyle,
|
|
107
|
+
tokenBodyFormat,
|
|
108
|
+
revocationUrl,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
6
111
|
|
|
7
|
-
function normalizeOAuthApp(row:
|
|
112
|
+
function normalizeOAuthApp(row: OAuthAppRow): OAuthApp {
|
|
113
|
+
const encrypted = row.clientSecretEncrypted === 1;
|
|
8
114
|
return {
|
|
9
115
|
...row,
|
|
116
|
+
clientSecret:
|
|
117
|
+
row.clientSecret == null
|
|
118
|
+
? ""
|
|
119
|
+
: encrypted
|
|
120
|
+
? decryptSecret(row.clientSecret, getEncryptionKey())
|
|
121
|
+
: row.clientSecret,
|
|
122
|
+
clientSecretEncrypted: encrypted,
|
|
123
|
+
scopes: parseScopeList(row.scopes).join(","),
|
|
124
|
+
requiresRefreshTokenRotation: row.requiresRefreshTokenRotation === 1,
|
|
10
125
|
createdAt: normalizeDateRequired(row.createdAt),
|
|
11
126
|
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
12
127
|
};
|
|
13
128
|
}
|
|
14
129
|
|
|
15
|
-
function
|
|
130
|
+
function normalizeAuthorization(row: OAuthAuthorizationRow): OAuthAuthorization {
|
|
131
|
+
const encrypted = row.tokensEncrypted === 1;
|
|
132
|
+
const key = encrypted ? getEncryptionKey() : null;
|
|
16
133
|
return {
|
|
17
134
|
...row,
|
|
135
|
+
accessToken: key ? decryptSecret(row.accessToken, key) : row.accessToken,
|
|
136
|
+
refreshToken:
|
|
137
|
+
row.refreshToken == null
|
|
138
|
+
? null
|
|
139
|
+
: key
|
|
140
|
+
? decryptSecret(row.refreshToken, key)
|
|
141
|
+
: row.refreshToken,
|
|
142
|
+
tokensEncrypted: encrypted,
|
|
18
143
|
createdAt: normalizeDateRequired(row.createdAt),
|
|
19
144
|
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
20
145
|
};
|
|
21
146
|
}
|
|
22
147
|
|
|
148
|
+
function rawOAuthAppByProvider(provider: string): OAuthAppRow | null {
|
|
149
|
+
return getDb()
|
|
150
|
+
.query(
|
|
151
|
+
`SELECT * FROM oauth_apps
|
|
152
|
+
WHERE provider = ? AND mcpServerId IS NULL
|
|
153
|
+
ORDER BY createdAt ASC, id ASC
|
|
154
|
+
LIMIT 1`,
|
|
155
|
+
)
|
|
156
|
+
.get(provider) as OAuthAppRow | null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Exact (non-MCP) app lookup by id — used to target a specific row when the
|
|
160
|
+
* provider is ambiguous (N apps per provider are allowed). */
|
|
161
|
+
function rawOAuthAppById(id: string): OAuthAppRow | null {
|
|
162
|
+
return getDb()
|
|
163
|
+
.query("SELECT * FROM oauth_apps WHERE id = ? AND mcpServerId IS NULL")
|
|
164
|
+
.get(id) as OAuthAppRow | null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function rawDefaultAuthorizationForApp(appId: string): OAuthAuthorizationRow | null {
|
|
168
|
+
return getDb()
|
|
169
|
+
.query("SELECT * FROM oauth_authorizations WHERE appId = ? AND label = 'default'")
|
|
170
|
+
.get(appId) as OAuthAuthorizationRow | null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function rawDefaultAuthorizationForProvider(provider: string): OAuthAuthorizationRow | null {
|
|
174
|
+
return getDb()
|
|
175
|
+
.query(
|
|
176
|
+
`SELECT z.*
|
|
177
|
+
FROM oauth_authorizations z
|
|
178
|
+
JOIN oauth_apps a ON a.id = z.appId
|
|
179
|
+
WHERE a.provider = ? AND a.mcpServerId IS NULL AND z.label = 'default'
|
|
180
|
+
ORDER BY a.createdAt ASC, a.id ASC
|
|
181
|
+
LIMIT 1`,
|
|
182
|
+
)
|
|
183
|
+
.get(provider) as OAuthAuthorizationRow | null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function getDefaultAuthorizationIdForProvider(provider: string): string | null {
|
|
187
|
+
return rawDefaultAuthorizationForProvider(provider)?.id ?? null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── OAuth Apps ──
|
|
191
|
+
|
|
23
192
|
export function getOAuthApp(provider: string): OAuthApp | null {
|
|
24
|
-
const row =
|
|
25
|
-
|
|
26
|
-
|
|
193
|
+
const row = rawOAuthAppByProvider(provider);
|
|
194
|
+
return row ? normalizeOAuthApp(row) : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getOAuthAppById(id: string): OAuthApp | null {
|
|
198
|
+
const row = getDb().query("SELECT * FROM oauth_apps WHERE id = ?").get(id) as OAuthAppRow | null;
|
|
27
199
|
return row ? normalizeOAuthApp(row) : null;
|
|
28
200
|
}
|
|
29
201
|
|
|
30
|
-
|
|
202
|
+
/** Resolve the (non-MCP) app id for a provider slug, or null if none. */
|
|
203
|
+
export function getOAuthAppIdByProvider(provider: string): string | null {
|
|
204
|
+
return rawOAuthAppByProvider(provider)?.id ?? null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Shared write payload for the OAuth-app create / update / provider-upsert
|
|
208
|
+
* paths. The row-resolution strategy is chosen by the calling function — never
|
|
209
|
+
* by this payload — so `id` is intentionally absent here. */
|
|
210
|
+
type OAuthAppWriteData = {
|
|
211
|
+
clientId: string;
|
|
212
|
+
clientSecret: string;
|
|
213
|
+
authorizeUrl: string;
|
|
214
|
+
tokenUrl: string;
|
|
215
|
+
redirectUri: string;
|
|
216
|
+
scopes: string;
|
|
217
|
+
metadata?: string;
|
|
218
|
+
displayName?: string | null;
|
|
219
|
+
revocationUrl?: string | null;
|
|
220
|
+
userinfoUrl?: string | null;
|
|
221
|
+
scopeSeparator?: string;
|
|
222
|
+
tokenAuthStyle?: "body" | "basic";
|
|
223
|
+
tokenBodyFormat?: "form" | "json";
|
|
224
|
+
requiresRefreshTokenRotation?: boolean;
|
|
225
|
+
extraParams?: Record<string, string> | null;
|
|
226
|
+
source?: "manual" | "dcr" | "curated-prefill";
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** Insert a new row (when `existing` is null) or update the given row in place.
|
|
230
|
+
* Returns the row id either way. Callers pick the row-resolution strategy up
|
|
231
|
+
* front — this helper never resolves by provider on its own. */
|
|
232
|
+
function writeOAuthApp(
|
|
233
|
+
existing: OAuthAppRow | null,
|
|
31
234
|
provider: string,
|
|
32
|
-
data:
|
|
33
|
-
|
|
34
|
-
clientSecret: string;
|
|
35
|
-
authorizeUrl: string;
|
|
36
|
-
tokenUrl: string;
|
|
37
|
-
redirectUri: string;
|
|
38
|
-
scopes: string;
|
|
39
|
-
metadata?: string;
|
|
40
|
-
},
|
|
41
|
-
): void {
|
|
42
|
-
// metadata is treated as a runtime-owned column (cloudId, webhookIds, etc.
|
|
43
|
-
// are written by OAuth callback + webhook-register flows). On INSERT we
|
|
44
|
-
// seed it with whatever the caller passed (or "{}"); on UPDATE we ONLY
|
|
45
|
-
// overwrite when the caller explicitly provided one — otherwise the
|
|
46
|
-
// existing value is preserved across server restarts.
|
|
235
|
+
data: OAuthAppWriteData,
|
|
236
|
+
): string {
|
|
47
237
|
const metadataProvided = data.metadata !== undefined;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
238
|
+
const lifted = metadataProvided ? storageMetadata(data.metadata as string) : null;
|
|
239
|
+
const encryptedSecret = encryptSecret(data.clientSecret, getEncryptionKey());
|
|
240
|
+
const scopeSeparator =
|
|
241
|
+
data.scopeSeparator ?? existing?.scopeSeparator ?? (provider === "linear" ? "," : " ");
|
|
242
|
+
const rotation =
|
|
243
|
+
data.requiresRefreshTokenRotation ??
|
|
244
|
+
(existing ? existing.requiresRefreshTokenRotation === 1 : provider === "jira");
|
|
245
|
+
const tokenAuthStyle =
|
|
246
|
+
data.tokenAuthStyle ?? lifted?.tokenAuthStyle ?? existing?.tokenAuthStyle ?? "body";
|
|
247
|
+
const tokenBodyFormat =
|
|
248
|
+
data.tokenBodyFormat ?? lifted?.tokenBodyFormat ?? existing?.tokenBodyFormat ?? "form";
|
|
249
|
+
const extraParamsJson =
|
|
250
|
+
data.extraParams !== undefined
|
|
251
|
+
? data.extraParams == null
|
|
252
|
+
? null
|
|
253
|
+
: JSON.stringify(data.extraParams)
|
|
254
|
+
: lifted
|
|
255
|
+
? lifted.extraParamsJson
|
|
256
|
+
: (existing?.extraParamsJson ?? null);
|
|
257
|
+
const revocationUrl =
|
|
258
|
+
data.revocationUrl !== undefined
|
|
259
|
+
? data.revocationUrl
|
|
260
|
+
: lifted
|
|
261
|
+
? lifted.revocationUrl
|
|
262
|
+
: (existing?.revocationUrl ?? null);
|
|
263
|
+
|
|
264
|
+
if (existing) {
|
|
71
265
|
getDb()
|
|
72
|
-
.query(
|
|
266
|
+
.query(
|
|
267
|
+
`UPDATE oauth_apps SET
|
|
268
|
+
displayName = ?, clientId = ?, clientSecret = ?, clientSecretEncrypted = 1,
|
|
269
|
+
authorizeUrl = ?, tokenUrl = ?, revocationUrl = ?, userinfoUrl = ?,
|
|
270
|
+
redirectUri = ?, scopes = ?, scopeSeparator = ?, tokenAuthStyle = ?,
|
|
271
|
+
tokenBodyFormat = ?, requiresRefreshTokenRotation = ?, extraParamsJson = ?,
|
|
272
|
+
source = ?, metadata = ?, updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
273
|
+
WHERE id = ?`,
|
|
274
|
+
)
|
|
73
275
|
.run(
|
|
74
|
-
|
|
276
|
+
data.displayName !== undefined ? data.displayName : existing.displayName,
|
|
75
277
|
data.clientId,
|
|
76
|
-
|
|
278
|
+
encryptedSecret,
|
|
77
279
|
data.authorizeUrl,
|
|
78
280
|
data.tokenUrl,
|
|
281
|
+
revocationUrl,
|
|
282
|
+
data.userinfoUrl !== undefined ? data.userinfoUrl : existing.userinfoUrl,
|
|
79
283
|
data.redirectUri,
|
|
80
|
-
data.scopes,
|
|
81
|
-
|
|
284
|
+
storeScopeList(data.scopes),
|
|
285
|
+
scopeSeparator,
|
|
286
|
+
tokenAuthStyle,
|
|
287
|
+
tokenBodyFormat,
|
|
288
|
+
rotation ? 1 : 0,
|
|
289
|
+
extraParamsJson,
|
|
290
|
+
data.source ?? existing.source,
|
|
291
|
+
lifted?.metadata ?? existing.metadata,
|
|
292
|
+
existing.id,
|
|
82
293
|
);
|
|
83
|
-
|
|
294
|
+
return existing.id;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const inserted = getDb()
|
|
298
|
+
.query(
|
|
299
|
+
`INSERT INTO oauth_apps (
|
|
300
|
+
provider, displayName, clientId, clientSecret, clientSecretEncrypted,
|
|
301
|
+
authorizeUrl, tokenUrl, revocationUrl, userinfoUrl, redirectUri, scopes,
|
|
302
|
+
scopeSeparator, tokenAuthStyle, tokenBodyFormat,
|
|
303
|
+
requiresRefreshTokenRotation, extraParamsJson, source, metadata
|
|
304
|
+
) VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
305
|
+
RETURNING id`,
|
|
306
|
+
)
|
|
307
|
+
.get(
|
|
308
|
+
provider,
|
|
309
|
+
data.displayName ?? null,
|
|
310
|
+
data.clientId,
|
|
311
|
+
encryptedSecret,
|
|
312
|
+
data.authorizeUrl,
|
|
313
|
+
data.tokenUrl,
|
|
314
|
+
revocationUrl,
|
|
315
|
+
data.userinfoUrl ?? null,
|
|
316
|
+
data.redirectUri,
|
|
317
|
+
storeScopeList(data.scopes),
|
|
318
|
+
scopeSeparator,
|
|
319
|
+
tokenAuthStyle,
|
|
320
|
+
tokenBodyFormat,
|
|
321
|
+
rotation ? 1 : 0,
|
|
322
|
+
extraParamsJson,
|
|
323
|
+
data.source ?? "manual",
|
|
324
|
+
lifted?.metadata ?? "{}",
|
|
325
|
+
) as { id: string };
|
|
326
|
+
return inserted.id;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Create a brand-new OAuth app row and return its id. ALWAYS inserts — it never
|
|
330
|
+
* resolves by provider, so a second app for the same provider can never clobber
|
|
331
|
+
* the first. User-facing create paths (HTTP `POST /api/oauth-apps` without an
|
|
332
|
+
* id, the MCP `oauth-app-upsert` action) MUST use this. */
|
|
333
|
+
export function createOAuthApp(provider: string, data: OAuthAppWriteData): string {
|
|
334
|
+
return writeOAuthApp(null, provider, data);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Update exactly one existing (non-MCP) app by id. Throws when the id is
|
|
338
|
+
* unknown or refers to an MCP-managed app. Provider is immutable on edit. */
|
|
339
|
+
export function updateOAuthAppById(id: string, data: OAuthAppWriteData): void {
|
|
340
|
+
const existing = rawOAuthAppById(id);
|
|
341
|
+
if (!existing) {
|
|
342
|
+
throw new Error(`OAuth app ${id} not found.`);
|
|
343
|
+
}
|
|
344
|
+
writeOAuthApp(existing, existing.provider, data);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Provider-keyed upsert — reserved for BOOT reconciliation (initLinear /
|
|
348
|
+
* initJira) and test seeding. Updates the oldest row for the provider, or
|
|
349
|
+
* inserts when none exists. Do NOT use for user-facing create/edit: with N apps
|
|
350
|
+
* per provider this silently clobbers a sibling row — the create path must use
|
|
351
|
+
* createOAuthApp and the edit path updateOAuthAppById. */
|
|
352
|
+
export function upsertOAuthApp(provider: string, data: OAuthAppWriteData): void {
|
|
353
|
+
writeOAuthApp(rawOAuthAppByProvider(provider), provider, data);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── OAuth Authorizations ──
|
|
357
|
+
|
|
358
|
+
export function listAuthorizationsForApp(appId: string): OAuthAuthorization[] {
|
|
359
|
+
const rows = getDb()
|
|
360
|
+
.query("SELECT * FROM oauth_authorizations WHERE appId = ? ORDER BY createdAt ASC, id ASC")
|
|
361
|
+
.all(appId) as OAuthAuthorizationRow[];
|
|
362
|
+
return rows.map(normalizeAuthorization);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function getAuthorizationById(id: string): OAuthAuthorization | null {
|
|
366
|
+
const row = getDb()
|
|
367
|
+
.query("SELECT * FROM oauth_authorizations WHERE id = ?")
|
|
368
|
+
.get(id) as OAuthAuthorizationRow | null;
|
|
369
|
+
return row ? normalizeAuthorization(row) : null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function upsertAuthorization(data: {
|
|
373
|
+
id?: string;
|
|
374
|
+
appId: string;
|
|
375
|
+
label?: string;
|
|
376
|
+
userId?: string | null;
|
|
377
|
+
accountEmail?: string | null;
|
|
378
|
+
identityJson?: string | null;
|
|
379
|
+
accessToken: string;
|
|
380
|
+
refreshToken?: string | null;
|
|
381
|
+
tokenType?: string;
|
|
382
|
+
expiresAt?: string | null;
|
|
383
|
+
scope?: string | null;
|
|
384
|
+
status?: OAuthAuthorizationStatus;
|
|
385
|
+
lastErrorMessage?: string | null;
|
|
386
|
+
lastRefreshedAt?: string | null;
|
|
387
|
+
connectedByUserId?: string | null;
|
|
388
|
+
}): OAuthAuthorization {
|
|
389
|
+
const label = data.label ?? "default";
|
|
390
|
+
const existing = data.id
|
|
391
|
+
? getAuthorizationById(data.id)
|
|
392
|
+
: listAuthorizationsForApp(data.appId).find((row) => row.label === label);
|
|
393
|
+
const key = getEncryptionKey();
|
|
394
|
+
const accessToken = encryptSecret(data.accessToken, key);
|
|
395
|
+
const refreshToken =
|
|
396
|
+
data.refreshToken === undefined
|
|
397
|
+
? undefined
|
|
398
|
+
: data.refreshToken == null
|
|
399
|
+
? null
|
|
400
|
+
: encryptSecret(data.refreshToken, key);
|
|
401
|
+
|
|
402
|
+
if (existing) {
|
|
84
403
|
getDb()
|
|
85
|
-
.query(
|
|
404
|
+
.query(
|
|
405
|
+
`UPDATE oauth_authorizations SET
|
|
406
|
+
userId = ?, accountEmail = ?, identityJson = ?, accessToken = ?,
|
|
407
|
+
refreshToken = ?, tokenType = ?, expiresAt = ?, scope = ?,
|
|
408
|
+
tokensEncrypted = 1, tokenVersion = tokenVersion + 1, status = ?,
|
|
409
|
+
lastErrorMessage = ?, lastRefreshedAt = ?, connectedByUserId = ?,
|
|
410
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
411
|
+
WHERE id = ?`,
|
|
412
|
+
)
|
|
86
413
|
.run(
|
|
87
|
-
|
|
88
|
-
data.
|
|
89
|
-
data.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
414
|
+
data.userId !== undefined ? data.userId : existing.userId,
|
|
415
|
+
data.accountEmail !== undefined ? data.accountEmail : existing.accountEmail,
|
|
416
|
+
data.identityJson !== undefined ? data.identityJson : existing.identityJson,
|
|
417
|
+
accessToken,
|
|
418
|
+
refreshToken === undefined
|
|
419
|
+
? existing.refreshToken == null
|
|
420
|
+
? null
|
|
421
|
+
: encryptSecret(existing.refreshToken, key)
|
|
422
|
+
: refreshToken,
|
|
423
|
+
data.tokenType ?? existing.tokenType,
|
|
424
|
+
data.expiresAt !== undefined ? data.expiresAt : existing.expiresAt,
|
|
425
|
+
data.scope !== undefined ? data.scope : existing.scope,
|
|
426
|
+
data.status ?? existing.status,
|
|
427
|
+
data.lastErrorMessage !== undefined ? data.lastErrorMessage : existing.lastErrorMessage,
|
|
428
|
+
data.lastRefreshedAt !== undefined ? data.lastRefreshedAt : existing.lastRefreshedAt,
|
|
429
|
+
data.connectedByUserId !== undefined ? data.connectedByUserId : existing.connectedByUserId,
|
|
430
|
+
existing.id,
|
|
94
431
|
);
|
|
432
|
+
return getAuthorizationById(existing.id) as OAuthAuthorization;
|
|
95
433
|
}
|
|
434
|
+
|
|
435
|
+
const id = data.id ?? crypto.randomUUID();
|
|
436
|
+
getDb()
|
|
437
|
+
.query(
|
|
438
|
+
`INSERT INTO oauth_authorizations (
|
|
439
|
+
id, appId, label, userId, accountEmail, identityJson,
|
|
440
|
+
accessToken, refreshToken, tokenType, expiresAt, scope,
|
|
441
|
+
tokensEncrypted, tokenVersion, status, lastErrorMessage,
|
|
442
|
+
lastRefreshedAt, connectedByUserId
|
|
443
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?, ?, ?)`,
|
|
444
|
+
)
|
|
445
|
+
.run(
|
|
446
|
+
id,
|
|
447
|
+
data.appId,
|
|
448
|
+
label,
|
|
449
|
+
data.userId ?? null,
|
|
450
|
+
data.accountEmail ?? null,
|
|
451
|
+
data.identityJson ?? null,
|
|
452
|
+
accessToken,
|
|
453
|
+
refreshToken ?? null,
|
|
454
|
+
data.tokenType ?? "Bearer",
|
|
455
|
+
data.expiresAt ?? null,
|
|
456
|
+
data.scope ?? null,
|
|
457
|
+
data.status ?? "active",
|
|
458
|
+
data.lastErrorMessage ?? null,
|
|
459
|
+
data.lastRefreshedAt ?? null,
|
|
460
|
+
data.connectedByUserId ?? null,
|
|
461
|
+
);
|
|
462
|
+
return getAuthorizationById(id) as OAuthAuthorization;
|
|
96
463
|
}
|
|
97
464
|
|
|
98
|
-
|
|
465
|
+
export function updateAuthorizationTokens(
|
|
466
|
+
id: string,
|
|
467
|
+
data: {
|
|
468
|
+
accessToken: string;
|
|
469
|
+
refreshToken?: string | null;
|
|
470
|
+
expiresAt?: string | null;
|
|
471
|
+
scope?: string | null;
|
|
472
|
+
expectedTokenVersion?: number;
|
|
473
|
+
},
|
|
474
|
+
): OAuthAuthorization | null {
|
|
475
|
+
const existing = getAuthorizationById(id);
|
|
476
|
+
if (!existing) return null;
|
|
477
|
+
const expectedVersion = data.expectedTokenVersion ?? existing.tokenVersion;
|
|
478
|
+
const key = getEncryptionKey();
|
|
479
|
+
const encryptedRefresh =
|
|
480
|
+
data.refreshToken === undefined
|
|
481
|
+
? existing.refreshToken == null
|
|
482
|
+
? null
|
|
483
|
+
: encryptSecret(existing.refreshToken, key)
|
|
484
|
+
: data.refreshToken == null
|
|
485
|
+
? null
|
|
486
|
+
: encryptSecret(data.refreshToken, key);
|
|
487
|
+
const result = getDb()
|
|
488
|
+
.query(
|
|
489
|
+
`UPDATE oauth_authorizations SET
|
|
490
|
+
accessToken = ?, refreshToken = ?, expiresAt = ?, scope = ?,
|
|
491
|
+
tokensEncrypted = 1, tokenVersion = tokenVersion + 1,
|
|
492
|
+
status = 'active', lastErrorMessage = NULL,
|
|
493
|
+
lastRefreshedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
|
494
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
495
|
+
WHERE id = ? AND tokenVersion = ?`,
|
|
496
|
+
)
|
|
497
|
+
.run(
|
|
498
|
+
encryptSecret(data.accessToken, key),
|
|
499
|
+
encryptedRefresh,
|
|
500
|
+
data.expiresAt !== undefined ? data.expiresAt : existing.expiresAt,
|
|
501
|
+
data.scope !== undefined ? data.scope : existing.scope,
|
|
502
|
+
id,
|
|
503
|
+
expectedVersion,
|
|
504
|
+
);
|
|
505
|
+
return result.changes === 1 ? getAuthorizationById(id) : null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Persist best-effort account identity (email + raw identity claims) captured
|
|
510
|
+
* after a successful token exchange. Never touches token material or
|
|
511
|
+
* tokenVersion — display-only metadata.
|
|
512
|
+
*/
|
|
513
|
+
export function updateAuthorizationIdentity(
|
|
514
|
+
id: string,
|
|
515
|
+
data: { accountEmail?: string | null; identityJson?: string | null },
|
|
516
|
+
): void {
|
|
517
|
+
getDb()
|
|
518
|
+
.query(
|
|
519
|
+
`UPDATE oauth_authorizations SET
|
|
520
|
+
accountEmail = ?, identityJson = ?,
|
|
521
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
522
|
+
WHERE id = ?`,
|
|
523
|
+
)
|
|
524
|
+
.run(data.accountEmail ?? null, data.identityJson ?? null, id);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Hard-delete a single authorization row (used by the multi-authorization
|
|
529
|
+
* DELETE endpoint after best-effort remote revocation). Bindings referencing
|
|
530
|
+
* it are detached via `ON DELETE SET NULL`.
|
|
531
|
+
*/
|
|
532
|
+
export function deleteAuthorizationById(id: string): boolean {
|
|
533
|
+
const result = getDb().query("DELETE FROM oauth_authorizations WHERE id = ?").run(id);
|
|
534
|
+
return result.changes > 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Delete every authorization owned by a SPECIFIC app id. Scoped by app id (not
|
|
539
|
+
* provider) so deleting one of several same-provider apps never touches a
|
|
540
|
+
* sibling app's authorizations. Bindings referencing the rows detach via
|
|
541
|
+
* `ON DELETE SET NULL`.
|
|
542
|
+
*/
|
|
543
|
+
export function deleteAuthorizationsForApp(appId: string): number {
|
|
544
|
+
return getDb().query("DELETE FROM oauth_authorizations WHERE appId = ?").run(appId).changes;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── OAuth pending (DB-backed PKCE state for generic/tracker flows) ──
|
|
548
|
+
|
|
549
|
+
/** Pending PKCE sessions are valid for 10 minutes (matches the GC window). */
|
|
550
|
+
export const OAUTH_PENDING_TTL_MS = 10 * 60 * 1000;
|
|
551
|
+
|
|
552
|
+
export type OAuthPendingFlow = "generic" | "tracker";
|
|
553
|
+
|
|
554
|
+
export interface OAuthPendingRecord {
|
|
555
|
+
state: string;
|
|
556
|
+
appId: string;
|
|
557
|
+
label: string;
|
|
558
|
+
flow: OAuthPendingFlow;
|
|
559
|
+
/** Decrypted PKCE code verifier. */
|
|
560
|
+
codeVerifier: string;
|
|
561
|
+
nonce: string | null;
|
|
562
|
+
redirectUri: string;
|
|
563
|
+
finalRedirect: string | null;
|
|
564
|
+
userId: string | null;
|
|
565
|
+
contextJson: string;
|
|
566
|
+
createdAt: string;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
type OAuthPendingRow = {
|
|
570
|
+
state: string;
|
|
571
|
+
appId: string;
|
|
572
|
+
label: string;
|
|
573
|
+
flow: OAuthPendingFlow;
|
|
574
|
+
codeVerifier: string;
|
|
575
|
+
nonce: string | null;
|
|
576
|
+
redirectUri: string;
|
|
577
|
+
finalRedirect: string | null;
|
|
578
|
+
userId: string | null;
|
|
579
|
+
contextJson: string;
|
|
580
|
+
createdAt: string;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
/** Persist a pending PKCE session for a generic/tracker OAuth flow. */
|
|
584
|
+
export function createOAuthPending(input: {
|
|
585
|
+
state: string;
|
|
586
|
+
appId: string;
|
|
587
|
+
label?: string;
|
|
588
|
+
flow?: OAuthPendingFlow;
|
|
589
|
+
/** Plaintext PKCE code verifier — encrypted at rest. */
|
|
590
|
+
codeVerifier: string;
|
|
591
|
+
nonce?: string | null;
|
|
592
|
+
redirectUri: string;
|
|
593
|
+
finalRedirect?: string | null;
|
|
594
|
+
userId?: string | null;
|
|
595
|
+
contextJson?: string;
|
|
596
|
+
}): void {
|
|
597
|
+
getDb()
|
|
598
|
+
.query(
|
|
599
|
+
`INSERT INTO oauth_pending (
|
|
600
|
+
state, appId, label, flow, codeVerifier, nonce,
|
|
601
|
+
redirectUri, finalRedirect, userId, contextJson
|
|
602
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
603
|
+
)
|
|
604
|
+
.run(
|
|
605
|
+
input.state,
|
|
606
|
+
input.appId,
|
|
607
|
+
input.label ?? "default",
|
|
608
|
+
input.flow ?? "generic",
|
|
609
|
+
encryptSecret(input.codeVerifier, getEncryptionKey()),
|
|
610
|
+
input.nonce ?? null,
|
|
611
|
+
input.redirectUri,
|
|
612
|
+
input.finalRedirect ?? null,
|
|
613
|
+
input.userId ?? null,
|
|
614
|
+
input.contextJson ?? "{}",
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Single-use consume of a generic/tracker pending row by `state`. Returns null
|
|
620
|
+
* for unknown states and for `mcp`-flow rows (those are owned by the MCP
|
|
621
|
+
* adapter), leaving the row untouched so the caller can fall through.
|
|
622
|
+
*
|
|
623
|
+
* Enforces the 10-minute TTL at consume time: an expired row is deleted and
|
|
624
|
+
* treated as invalid, so a stalled/failed GC timer cannot widen the validity
|
|
625
|
+
* window.
|
|
626
|
+
*/
|
|
627
|
+
export function consumeOAuthPending(state: string): OAuthPendingRecord | null {
|
|
628
|
+
return getDb().transaction(() => {
|
|
629
|
+
const row = getDb()
|
|
630
|
+
.query(
|
|
631
|
+
`SELECT state, appId, label, flow, codeVerifier, nonce, redirectUri,
|
|
632
|
+
finalRedirect, userId, contextJson, createdAt
|
|
633
|
+
FROM oauth_pending
|
|
634
|
+
WHERE state = ? AND flow IN ('generic', 'tracker')`,
|
|
635
|
+
)
|
|
636
|
+
.get(state) as OAuthPendingRow | null;
|
|
637
|
+
if (!row) return null;
|
|
638
|
+
// Single-use: always delete, whether valid or expired.
|
|
639
|
+
getDb().query("DELETE FROM oauth_pending WHERE state = ?").run(state);
|
|
640
|
+
const createdAt = normalizeDateRequired(row.createdAt);
|
|
641
|
+
const createdMs = new Date(createdAt).getTime();
|
|
642
|
+
if (Number.isNaN(createdMs) || Date.now() - createdMs > OAUTH_PENDING_TTL_MS) {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
...row,
|
|
647
|
+
codeVerifier: decryptSecret(row.codeVerifier, getEncryptionKey()),
|
|
648
|
+
createdAt,
|
|
649
|
+
};
|
|
650
|
+
})();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** GC expired generic/tracker pending rows. MCP rows are GC'd separately. */
|
|
654
|
+
export function gcOAuthPending(olderThanMs = 10 * 60 * 1000): number {
|
|
655
|
+
const cutoff = new Date(Date.now() - olderThanMs).toISOString();
|
|
656
|
+
return getDb()
|
|
657
|
+
.query("DELETE FROM oauth_pending WHERE flow IN ('generic', 'tracker') AND createdAt < ?")
|
|
658
|
+
.run(cutoff).changes;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Persist a `refresh-failed` status + scrubbed error message on an
|
|
663
|
+
* authorization whose refresh just failed. Never clobbers a `revoked` row (a
|
|
664
|
+
* disconnected authorization is terminal, not "failing"). Best-effort by id —
|
|
665
|
+
* no optimistic-concurrency guard, since a successful refresh always bumps
|
|
666
|
+
* status back to `active` and clears the message anyway.
|
|
667
|
+
*/
|
|
668
|
+
export function markAuthorizationRefreshFailed(
|
|
669
|
+
id: string,
|
|
670
|
+
message: string | null,
|
|
671
|
+
): OAuthAuthorization | null {
|
|
672
|
+
const result = getDb()
|
|
673
|
+
.query(
|
|
674
|
+
`UPDATE oauth_authorizations SET
|
|
675
|
+
status = 'refresh-failed', lastErrorMessage = ?,
|
|
676
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
677
|
+
WHERE id = ? AND status != 'revoked'`,
|
|
678
|
+
)
|
|
679
|
+
.run(message, id);
|
|
680
|
+
return result.changes === 1 ? getAuthorizationById(id) : null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ── Provider-string compatibility adapters ──
|
|
99
684
|
|
|
100
685
|
export function getOAuthTokens(provider: string): OAuthTokens | null {
|
|
101
|
-
const row =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
686
|
+
const row = rawDefaultAuthorizationForProvider(provider);
|
|
687
|
+
if (!row) return null;
|
|
688
|
+
const authorization = normalizeAuthorization(row);
|
|
689
|
+
// A revoked authorization is a disconnected connection: the row is kept for
|
|
690
|
+
// referential continuity (script_credential_bindings.oauth_authorization_id,
|
|
691
|
+
// ON DELETE SET NULL) but must read as "no tokens" to provider-string callers.
|
|
692
|
+
if (authorization.status === "revoked") return null;
|
|
693
|
+
return {
|
|
694
|
+
id: authorization.id,
|
|
695
|
+
provider,
|
|
696
|
+
accessToken: authorization.accessToken,
|
|
697
|
+
refreshToken: authorization.refreshToken,
|
|
698
|
+
expiresAt: authorization.expiresAt ?? "",
|
|
699
|
+
scope: authorization.scope,
|
|
700
|
+
tokenVersion: authorization.tokenVersion,
|
|
701
|
+
createdAt: authorization.createdAt,
|
|
702
|
+
updatedAt: authorization.updatedAt,
|
|
703
|
+
};
|
|
105
704
|
}
|
|
106
705
|
|
|
107
706
|
export function storeOAuthTokens(
|
|
@@ -109,23 +708,25 @@ export function storeOAuthTokens(
|
|
|
109
708
|
data: {
|
|
110
709
|
accessToken: string;
|
|
111
710
|
refreshToken?: string | null;
|
|
112
|
-
expiresAt: string;
|
|
711
|
+
expiresAt: string | null;
|
|
113
712
|
scope?: string | null;
|
|
114
713
|
},
|
|
115
714
|
): void {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
715
|
+
const app = rawOAuthAppByProvider(provider);
|
|
716
|
+
if (!app) throw new Error(`OAuth app ${provider} is not configured`);
|
|
717
|
+
const existing = rawDefaultAuthorizationForApp(app.id);
|
|
718
|
+
upsertAuthorization({
|
|
719
|
+
...(existing ? { id: existing.id } : {}),
|
|
720
|
+
appId: app.id,
|
|
721
|
+
label: "default",
|
|
722
|
+
accessToken: data.accessToken,
|
|
723
|
+
...(data.refreshToken != null && data.refreshToken !== ""
|
|
724
|
+
? { refreshToken: data.refreshToken }
|
|
725
|
+
: {}),
|
|
726
|
+
expiresAt: data.expiresAt,
|
|
727
|
+
...(data.scope != null ? { scope: data.scope } : {}),
|
|
728
|
+
status: "active",
|
|
729
|
+
});
|
|
129
730
|
}
|
|
130
731
|
|
|
131
732
|
export function updateOAuthTokensAfterRefresh(
|
|
@@ -134,98 +735,179 @@ export function updateOAuthTokensAfterRefresh(
|
|
|
134
735
|
data: {
|
|
135
736
|
accessToken: string;
|
|
136
737
|
refreshToken: string;
|
|
137
|
-
expiresAt: string;
|
|
738
|
+
expiresAt: string | null;
|
|
138
739
|
scope?: string | null;
|
|
740
|
+
expectedTokenVersion?: number;
|
|
139
741
|
},
|
|
140
742
|
): void {
|
|
141
|
-
const
|
|
142
|
-
.query(
|
|
143
|
-
`UPDATE oauth_tokens
|
|
144
|
-
SET accessToken = ?,
|
|
145
|
-
refreshToken = ?,
|
|
146
|
-
expiresAt = ?,
|
|
147
|
-
scope = COALESCE(?, scope),
|
|
148
|
-
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
149
|
-
WHERE provider = ? AND refreshToken = ?`,
|
|
150
|
-
)
|
|
151
|
-
.run(
|
|
152
|
-
data.accessToken,
|
|
153
|
-
data.refreshToken,
|
|
154
|
-
data.expiresAt,
|
|
155
|
-
data.scope ?? null,
|
|
156
|
-
provider,
|
|
157
|
-
expectedRefreshToken,
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
if (result.changes === 1) return;
|
|
161
|
-
|
|
162
|
-
const current = getOAuthTokens(provider);
|
|
743
|
+
const current = rawDefaultAuthorizationForProvider(provider);
|
|
163
744
|
if (!current) {
|
|
164
745
|
throw new Error(`OAuth token refresh persistence failed for ${provider}: token row missing`);
|
|
165
746
|
}
|
|
166
|
-
|
|
747
|
+
const normalized = normalizeAuthorization(current);
|
|
748
|
+
if (normalized.refreshToken !== expectedRefreshToken) {
|
|
167
749
|
throw new Error(
|
|
168
750
|
`OAuth token refresh persistence failed for ${provider}: stored refresh token changed during refresh`,
|
|
169
751
|
);
|
|
170
752
|
}
|
|
171
|
-
|
|
753
|
+
const updated = updateAuthorizationTokens(current.id, {
|
|
754
|
+
accessToken: data.accessToken,
|
|
755
|
+
refreshToken: data.refreshToken,
|
|
756
|
+
expiresAt: data.expiresAt,
|
|
757
|
+
...(data.scope != null ? { scope: data.scope } : {}),
|
|
758
|
+
expectedTokenVersion: data.expectedTokenVersion ?? current.tokenVersion,
|
|
759
|
+
});
|
|
760
|
+
if (updated) return;
|
|
761
|
+
|
|
762
|
+
const latest = getAuthorizationById(current.id);
|
|
763
|
+
if (!latest) {
|
|
764
|
+
throw new Error(`OAuth token refresh persistence failed for ${provider}: token row missing`);
|
|
765
|
+
}
|
|
766
|
+
if (latest.refreshToken === expectedRefreshToken) {
|
|
767
|
+
throw new Error(`OAuth token refresh persistence failed for ${provider}: no rows updated`);
|
|
768
|
+
}
|
|
769
|
+
throw new Error(
|
|
770
|
+
`OAuth token refresh persistence failed for ${provider}: stored refresh token changed during refresh`,
|
|
771
|
+
);
|
|
172
772
|
}
|
|
173
773
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
* them into logs.
|
|
178
|
-
*/
|
|
179
|
-
export type OAuthTokenSweepRow = {
|
|
774
|
+
export type AuthorizationSweepRow = {
|
|
775
|
+
authorizationId: string;
|
|
776
|
+
appId: string;
|
|
180
777
|
provider: string;
|
|
181
|
-
|
|
778
|
+
label: string;
|
|
779
|
+
status: OAuthAuthorizationStatus;
|
|
182
780
|
hasRefreshToken: boolean;
|
|
183
781
|
expiresAt: string;
|
|
184
782
|
updatedAt: string;
|
|
185
783
|
};
|
|
186
784
|
|
|
187
|
-
|
|
785
|
+
/**
|
|
786
|
+
* Every provider-facing authorization (any label; DCR/MCP apps excluded) the
|
|
787
|
+
* background sweep should consider. Keyed by authorization id — the sweep skips
|
|
788
|
+
* `revoked` rows and refreshes `refresh-failed` ones each pass so transient
|
|
789
|
+
* provider outages self-heal.
|
|
790
|
+
*/
|
|
791
|
+
export function listAuthorizationSweepRows(): AuthorizationSweepRow[] {
|
|
188
792
|
const rows = getDb()
|
|
189
793
|
.query(
|
|
190
|
-
`SELECT
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
794
|
+
`SELECT z.id AS authorizationId,
|
|
795
|
+
z.appId AS appId,
|
|
796
|
+
a.provider AS provider,
|
|
797
|
+
z.label AS label,
|
|
798
|
+
z.status AS status,
|
|
799
|
+
CASE WHEN z.refreshToken IS NOT NULL AND z.refreshToken != '' THEN 1 ELSE 0 END AS hasRefreshToken,
|
|
800
|
+
COALESCE(z.expiresAt, '') AS expiresAt,
|
|
801
|
+
z.updatedAt AS updatedAt
|
|
802
|
+
FROM oauth_authorizations z
|
|
803
|
+
JOIN oauth_apps a ON a.id = z.appId
|
|
804
|
+
WHERE a.mcpServerId IS NULL
|
|
805
|
+
ORDER BY a.provider ASC, z.label ASC`,
|
|
198
806
|
)
|
|
199
807
|
.all() as Array<{
|
|
808
|
+
authorizationId: string;
|
|
809
|
+
appId: string;
|
|
200
810
|
provider: string;
|
|
201
|
-
|
|
811
|
+
label: string;
|
|
812
|
+
status: OAuthAuthorizationStatus;
|
|
202
813
|
hasRefreshToken: number;
|
|
203
814
|
expiresAt: string;
|
|
204
815
|
updatedAt: string;
|
|
205
816
|
}>;
|
|
206
817
|
return rows.map((row) => ({
|
|
818
|
+
authorizationId: row.authorizationId,
|
|
819
|
+
appId: row.appId,
|
|
207
820
|
provider: row.provider,
|
|
208
|
-
|
|
821
|
+
label: row.label,
|
|
822
|
+
status: row.status,
|
|
209
823
|
hasRefreshToken: row.hasRefreshToken === 1,
|
|
210
824
|
expiresAt: normalizeDateRequired(row.expiresAt),
|
|
211
825
|
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
212
826
|
}));
|
|
213
827
|
}
|
|
214
828
|
|
|
829
|
+
export type KeepAliveAuthorization = {
|
|
830
|
+
authorizationId: string;
|
|
831
|
+
appId: string;
|
|
832
|
+
provider: string;
|
|
833
|
+
label: string;
|
|
834
|
+
displayName: string | null;
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Active, refreshable authorizations whose app opts into keep-alive: either it
|
|
839
|
+
* rotates refresh tokens (`requiresRefreshTokenRotation=1`) or sets the
|
|
840
|
+
* `keepAlive` metadata flag. Replaces the hardcoded `["linear","jira"]` list;
|
|
841
|
+
* migrated tracker rows qualify automatically (jira via rotation, linear via
|
|
842
|
+
* the metadata backfill).
|
|
843
|
+
*/
|
|
844
|
+
export function listKeepAliveAuthorizations(): KeepAliveAuthorization[] {
|
|
845
|
+
const rows = getDb()
|
|
846
|
+
.query(
|
|
847
|
+
`SELECT z.id AS authorizationId,
|
|
848
|
+
z.appId AS appId,
|
|
849
|
+
a.provider AS provider,
|
|
850
|
+
z.label AS label,
|
|
851
|
+
a.displayName AS displayName
|
|
852
|
+
FROM oauth_authorizations z
|
|
853
|
+
JOIN oauth_apps a ON a.id = z.appId
|
|
854
|
+
WHERE a.mcpServerId IS NULL
|
|
855
|
+
AND z.status = 'active'
|
|
856
|
+
AND z.refreshToken IS NOT NULL AND z.refreshToken != ''
|
|
857
|
+
AND (
|
|
858
|
+
a.requiresRefreshTokenRotation = 1
|
|
859
|
+
OR (json_valid(a.metadata) = 1 AND json_extract(a.metadata, '$.keepAlive') IN (1, 'true'))
|
|
860
|
+
)
|
|
861
|
+
ORDER BY a.provider ASC, z.label ASC`,
|
|
862
|
+
)
|
|
863
|
+
.all() as Array<{
|
|
864
|
+
authorizationId: string;
|
|
865
|
+
appId: string;
|
|
866
|
+
provider: string;
|
|
867
|
+
label: string;
|
|
868
|
+
displayName: string | null;
|
|
869
|
+
}>;
|
|
870
|
+
return rows.map((row) => ({
|
|
871
|
+
authorizationId: row.authorizationId,
|
|
872
|
+
appId: row.appId,
|
|
873
|
+
provider: row.provider,
|
|
874
|
+
label: row.label,
|
|
875
|
+
displayName: row.displayName,
|
|
876
|
+
}));
|
|
877
|
+
}
|
|
878
|
+
|
|
215
879
|
export function deleteOAuthTokens(provider: string): void {
|
|
216
|
-
|
|
880
|
+
const app = rawOAuthAppByProvider(provider);
|
|
881
|
+
if (!app) return;
|
|
882
|
+
const existing = rawDefaultAuthorizationForApp(app.id);
|
|
883
|
+
if (!existing) return;
|
|
884
|
+
// Disconnect revokes in place — clear token fields and mark revoked but KEEP
|
|
885
|
+
// the row so script_credential_bindings.oauth_authorization_id survives and a
|
|
886
|
+
// later reconnect (upsert by appId+label='default') reuses the same id. Real
|
|
887
|
+
// deletion of an authorization only happens via oauth_apps CASCADE.
|
|
888
|
+
getDb()
|
|
889
|
+
.query(
|
|
890
|
+
`UPDATE oauth_authorizations SET
|
|
891
|
+
accessToken = ?, refreshToken = NULL, expiresAt = NULL, scope = NULL,
|
|
892
|
+
tokensEncrypted = 1, tokenVersion = tokenVersion + 1,
|
|
893
|
+
status = 'revoked', lastErrorMessage = NULL, lastRefreshedAt = NULL,
|
|
894
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
895
|
+
WHERE id = ?`,
|
|
896
|
+
)
|
|
897
|
+
.run(encryptSecret("", getEncryptionKey()), existing.id);
|
|
217
898
|
}
|
|
218
899
|
|
|
219
900
|
export function isTokenExpiringSoon(provider: string, bufferMs = 5 * 60 * 1000): boolean {
|
|
220
901
|
const tokens = getOAuthTokens(provider);
|
|
221
902
|
if (!tokens) return true;
|
|
222
903
|
const expiresAt = new Date(tokens.expiresAt).getTime();
|
|
904
|
+
if (Number.isNaN(expiresAt)) return true;
|
|
223
905
|
return expiresAt - Date.now() < bufferMs;
|
|
224
906
|
}
|
|
225
907
|
|
|
226
908
|
// ── OAuth Refresh Locks ──
|
|
227
909
|
|
|
228
|
-
export function acquireOAuthRefreshLock(
|
|
910
|
+
export function acquireOAuthRefreshLock(lockKey: string, ttlMs: number): string | null {
|
|
229
911
|
const owner = crypto.randomUUID();
|
|
230
912
|
const now = Date.now();
|
|
231
913
|
const expiresAt = new Date(now + ttlMs).toISOString();
|
|
@@ -233,25 +915,25 @@ export function acquireOAuthRefreshLock(provider: string, ttlMs: number): string
|
|
|
233
915
|
|
|
234
916
|
getDb()
|
|
235
917
|
.query(
|
|
236
|
-
`INSERT INTO oauth_refresh_locks (
|
|
918
|
+
`INSERT INTO oauth_refresh_locks (lockKey, owner, expiresAt, createdAt, updatedAt)
|
|
237
919
|
VALUES (?, ?, ?, ?, ?)
|
|
238
|
-
ON CONFLICT(
|
|
920
|
+
ON CONFLICT(lockKey) DO UPDATE SET
|
|
239
921
|
owner = excluded.owner,
|
|
240
922
|
expiresAt = excluded.expiresAt,
|
|
241
923
|
updatedAt = excluded.updatedAt
|
|
242
924
|
WHERE oauth_refresh_locks.expiresAt <= ?`,
|
|
243
925
|
)
|
|
244
|
-
.run(
|
|
926
|
+
.run(lockKey, owner, expiresAt, nowIso, nowIso, nowIso);
|
|
245
927
|
|
|
246
928
|
const row = getDb()
|
|
247
|
-
.query("SELECT owner FROM oauth_refresh_locks WHERE
|
|
248
|
-
.get(
|
|
929
|
+
.query("SELECT owner FROM oauth_refresh_locks WHERE lockKey = ?")
|
|
930
|
+
.get(lockKey) as { owner: string } | null;
|
|
249
931
|
|
|
250
932
|
return row?.owner === owner ? owner : null;
|
|
251
933
|
}
|
|
252
934
|
|
|
253
|
-
export function releaseOAuthRefreshLock(
|
|
935
|
+
export function releaseOAuthRefreshLock(lockKey: string, owner: string): void {
|
|
254
936
|
getDb()
|
|
255
|
-
.query("DELETE FROM oauth_refresh_locks WHERE
|
|
256
|
-
.run(
|
|
937
|
+
.query("DELETE FROM oauth_refresh_locks WHERE lockKey = ? AND owner = ?")
|
|
938
|
+
.run(lockKey, owner);
|
|
257
939
|
}
|