@indecisivedev/openplan 0.2.0-alpha.2
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 +21 -0
- package/README.md +120 -0
- package/dist/access-policy.d.ts +35 -0
- package/dist/access-policy.js +107 -0
- package/dist/access-policy.js.map +1 -0
- package/dist/access-store.d.ts +33 -0
- package/dist/access-store.js +212 -0
- package/dist/access-store.js.map +1 -0
- package/dist/admin.d.ts +1 -0
- package/dist/admin.js +13 -0
- package/dist/admin.js.map +1 -0
- package/dist/api.d.ts +2 -0
- package/dist/api.js +867 -0
- package/dist/api.js.map +1 -0
- package/dist/bootstrap-admin.d.ts +1 -0
- package/dist/bootstrap-admin.js +35 -0
- package/dist/bootstrap-admin.js.map +1 -0
- package/dist/browser-security.d.ts +5 -0
- package/dist/browser-security.js +53 -0
- package/dist/browser-security.js.map +1 -0
- package/dist/celld-quickstart.d.ts +51 -0
- package/dist/celld-quickstart.js +1175 -0
- package/dist/celld-quickstart.js.map +1 -0
- package/dist/cli-api-client.d.ts +78 -0
- package/dist/cli-api-client.js +272 -0
- package/dist/cli-api-client.js.map +1 -0
- package/dist/cli-credentials.d.ts +47 -0
- package/dist/cli-credentials.js +447 -0
- package/dist/cli-credentials.js.map +1 -0
- package/dist/cli-endpoint.d.ts +19 -0
- package/dist/cli-endpoint.js +56 -0
- package/dist/cli-endpoint.js.map +1 -0
- package/dist/cli-program.d.ts +11 -0
- package/dist/cli-program.js +934 -0
- package/dist/cli-program.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-ip.d.ts +4 -0
- package/dist/client-ip.js +49 -0
- package/dist/client-ip.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +113 -0
- package/dist/config.js.map +1 -0
- package/dist/crypto.d.ts +3 -0
- package/dist/crypto.js +11 -0
- package/dist/crypto.js.map +1 -0
- package/dist/database.d.ts +15 -0
- package/dist/database.js +155 -0
- package/dist/database.js.map +1 -0
- package/dist/db.d.ts +31 -0
- package/dist/db.js +388 -0
- package/dist/db.js.map +1 -0
- package/dist/drafts.d.ts +43 -0
- package/dist/drafts.js +90 -0
- package/dist/drafts.js.map +1 -0
- package/dist/feature-settings.d.ts +7 -0
- package/dist/feature-settings.js +32 -0
- package/dist/feature-settings.js.map +1 -0
- package/dist/generated/annotation-client.d.ts +1 -0
- package/dist/generated/annotation-client.js +2 -0
- package/dist/generated/annotation-client.js.map +1 -0
- package/dist/html-policy.d.ts +15 -0
- package/dist/html-policy.js +172 -0
- package/dist/html-policy.js.map +1 -0
- package/dist/ids.d.ts +2 -0
- package/dist/ids.js +10 -0
- package/dist/ids.js.map +1 -0
- package/dist/local-auth.d.ts +36 -0
- package/dist/local-auth.js +215 -0
- package/dist/local-auth.js.map +1 -0
- package/dist/maintenance.d.ts +1 -0
- package/dist/maintenance.js +18 -0
- package/dist/maintenance.js.map +1 -0
- package/dist/oidc.d.ts +39 -0
- package/dist/oidc.js +194 -0
- package/dist/oidc.js.map +1 -0
- package/dist/public-url.d.ts +17 -0
- package/dist/public-url.js +96 -0
- package/dist/public-url.js.map +1 -0
- package/dist/rate-limit.d.ts +25 -0
- package/dist/rate-limit.js +55 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/reconcile.d.ts +5 -0
- package/dist/reconcile.js +18 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/render-annotations.d.ts +10 -0
- package/dist/render-annotations.js +73 -0
- package/dist/render-annotations.js.map +1 -0
- package/dist/render-web.d.ts +74 -0
- package/dist/render-web.js +439 -0
- package/dist/render-web.js.map +1 -0
- package/dist/render.d.ts +4 -0
- package/dist/render.js +85 -0
- package/dist/render.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +26 -0
- package/dist/server.js.map +1 -0
- package/dist/shoo.d.ts +26 -0
- package/dist/shoo.js +92 -0
- package/dist/shoo.js.map +1 -0
- package/dist/sqlite-migrations.d.ts +6 -0
- package/dist/sqlite-migrations.js +214 -0
- package/dist/sqlite-migrations.js.map +1 -0
- package/dist/storage.d.ts +14 -0
- package/dist/storage.js +86 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/upload-state.d.ts +19 -0
- package/dist/upload-state.js +118 -0
- package/dist/upload-state.js.map +1 -0
- package/dist/web-auth.d.ts +33 -0
- package/dist/web-auth.js +122 -0
- package/dist/web-auth.js.map +1 -0
- package/dist/web.d.ts +2 -0
- package/dist/web.js +550 -0
- package/dist/web.js.map +1 -0
- package/package.json +70 -0
package/dist/shoo.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
3
|
+
import { config } from "./config.js";
|
|
4
|
+
import { randomToken } from "./crypto.js";
|
|
5
|
+
// shoo (shoo.dev) protocol facts, extracted from its source:
|
|
6
|
+
// - Clients are auto-registered by redirect_uri origin; client_id is always
|
|
7
|
+
// derived as `origin:<origin>` and never needs a secret.
|
|
8
|
+
// - /authorize requires redirect_uri, state, code_challenge (S256 only).
|
|
9
|
+
// - /token takes application/x-www-form-urlencoded, and redirect_uri must be
|
|
10
|
+
// byte-identical to the one sent to /authorize. Codes are single-use, 120s.
|
|
11
|
+
// - The id_token is ES256; aud is `origin:<origin>`; the stable per-site user
|
|
12
|
+
// id is `pairwise_sub` (deterministic, survives revoke + re-auth).
|
|
13
|
+
// - The only error shoo redirects back is ?error=access_denied — everything
|
|
14
|
+
// else renders on shoo itself.
|
|
15
|
+
let jwksCache = null;
|
|
16
|
+
let issuerCache = null;
|
|
17
|
+
export function buildPkce() {
|
|
18
|
+
const verifier = randomToken(32);
|
|
19
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
20
|
+
return { verifier, challenge, state: randomToken(24) };
|
|
21
|
+
}
|
|
22
|
+
export function buildAuthorizeUrl({ redirectUri, state, challenge }) {
|
|
23
|
+
const url = new URL(`${config.shooBaseUrl}/authorize`);
|
|
24
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
25
|
+
url.searchParams.set("state", state);
|
|
26
|
+
url.searchParams.set("code_challenge", challenge);
|
|
27
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
28
|
+
// Request profile claims (email/email_verified/name/picture/pii_sub). shoo
|
|
29
|
+
// shows a one-time consent screen per user; declining it denies the sign-in.
|
|
30
|
+
url.searchParams.set("pii", "true");
|
|
31
|
+
return url.toString();
|
|
32
|
+
}
|
|
33
|
+
export async function exchangeCode({ code, verifier, redirectUri }) {
|
|
34
|
+
const response = await fetch(`${config.shooBaseUrl}/token`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
37
|
+
body: new URLSearchParams({
|
|
38
|
+
grant_type: "authorization_code",
|
|
39
|
+
redirect_uri: redirectUri,
|
|
40
|
+
code,
|
|
41
|
+
code_verifier: verifier
|
|
42
|
+
}),
|
|
43
|
+
signal: AbortSignal.timeout(10_000)
|
|
44
|
+
});
|
|
45
|
+
const parsed = await response.json().catch(() => ({}));
|
|
46
|
+
const body = parsed && typeof parsed === "object" ? parsed : {};
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`shoo token exchange failed: ${String(body.error || response.status)}`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof body.id_token !== "string") {
|
|
51
|
+
throw new Error("shoo token exchange returned no id_token.");
|
|
52
|
+
}
|
|
53
|
+
return body;
|
|
54
|
+
}
|
|
55
|
+
// Verifies the ES256 id_token against shoo's JWKS and returns its claims.
|
|
56
|
+
// audOrigin must be this deployment's public origin (e.g. https://openplan.dev).
|
|
57
|
+
export async function verifyIdToken(idToken, { audOrigin }) {
|
|
58
|
+
const audience = `origin:${new URL(audOrigin).origin}`;
|
|
59
|
+
const { payload } = await jwtVerify(idToken, getJwks(), {
|
|
60
|
+
issuer: await getIssuer(),
|
|
61
|
+
audience,
|
|
62
|
+
algorithms: ["ES256"]
|
|
63
|
+
});
|
|
64
|
+
if (typeof payload.pairwise_sub !== "string" || !payload.pairwise_sub) {
|
|
65
|
+
throw new Error("shoo id_token is missing pairwise_sub.");
|
|
66
|
+
}
|
|
67
|
+
return payload;
|
|
68
|
+
}
|
|
69
|
+
function getJwks() {
|
|
70
|
+
jwksCache ||= createRemoteJWKSet(new URL(`${config.shooBaseUrl}/.well-known/jwks.json`));
|
|
71
|
+
return jwksCache;
|
|
72
|
+
}
|
|
73
|
+
// The issuer string is whatever shoo's discovery document says (it may differ
|
|
74
|
+
// from the base URL), so fetch it once instead of assuming.
|
|
75
|
+
async function getIssuer() {
|
|
76
|
+
issuerCache ||= (async () => {
|
|
77
|
+
const response = await fetch(`${config.shooBaseUrl}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(10_000) });
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`shoo discovery failed: ${response.status}`);
|
|
80
|
+
}
|
|
81
|
+
const body = await response.json();
|
|
82
|
+
if (!body || typeof body !== "object" || typeof body.issuer !== "string") {
|
|
83
|
+
throw new Error("shoo discovery document has no issuer.");
|
|
84
|
+
}
|
|
85
|
+
return body.issuer;
|
|
86
|
+
})().catch((error) => {
|
|
87
|
+
issuerCache = null;
|
|
88
|
+
throw error;
|
|
89
|
+
});
|
|
90
|
+
return issuerCache;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=shoo.js.map
|
package/dist/shoo.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shoo.js","sourceRoot":"","sources":["../src/shoo.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
export const sqliteMigrations = [
|
|
2
|
+
{
|
|
3
|
+
version: 1,
|
|
4
|
+
name: "initial-self-hosted-schema",
|
|
5
|
+
sql: `
|
|
6
|
+
CREATE TABLE accounts (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
name TEXT NOT NULL,
|
|
9
|
+
is_admin INTEGER NOT NULL DEFAULT 0 CHECK (is_admin IN (0, 1)),
|
|
10
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
11
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE draft_access_policies (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
mode TEXT NOT NULL CHECK (mode IN ('private', 'password', 'public')),
|
|
17
|
+
password_algorithm TEXT,
|
|
18
|
+
password_iterations INTEGER,
|
|
19
|
+
password_salt TEXT,
|
|
20
|
+
password_hash TEXT,
|
|
21
|
+
credential_revision INTEGER NOT NULL DEFAULT 1,
|
|
22
|
+
created_by_account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
23
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
24
|
+
CHECK (
|
|
25
|
+
(mode = 'password' AND password_algorithm IS NOT NULL AND password_iterations IS NOT NULL
|
|
26
|
+
AND password_salt IS NOT NULL AND password_hash IS NOT NULL)
|
|
27
|
+
OR
|
|
28
|
+
(mode <> 'password' AND password_algorithm IS NULL AND password_iterations IS NULL
|
|
29
|
+
AND password_salt IS NULL AND password_hash IS NULL)
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE api_keys (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
36
|
+
name TEXT NOT NULL,
|
|
37
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
38
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
39
|
+
last_used_at TEXT,
|
|
40
|
+
revoked_at TEXT
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE drafts (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
46
|
+
title TEXT NOT NULL,
|
|
47
|
+
description TEXT,
|
|
48
|
+
current_version_id TEXT,
|
|
49
|
+
repo_org TEXT,
|
|
50
|
+
repo_name TEXT,
|
|
51
|
+
repo_host TEXT,
|
|
52
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
53
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
54
|
+
deleted_at TEXT,
|
|
55
|
+
disabled_at TEXT,
|
|
56
|
+
disabled_reason TEXT,
|
|
57
|
+
default_access_policy_id TEXT REFERENCES draft_access_policies(id)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE draft_versions (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
draft_id TEXT NOT NULL REFERENCES drafts(id),
|
|
63
|
+
version_number INTEGER NOT NULL,
|
|
64
|
+
object_key TEXT NOT NULL,
|
|
65
|
+
content_hash TEXT NOT NULL,
|
|
66
|
+
file_size INTEGER NOT NULL,
|
|
67
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
68
|
+
created_by_api_key_id TEXT NOT NULL REFERENCES api_keys(id),
|
|
69
|
+
source_ip TEXT,
|
|
70
|
+
user_agent TEXT,
|
|
71
|
+
cli_version TEXT,
|
|
72
|
+
git_branch TEXT,
|
|
73
|
+
git_commit_sha TEXT,
|
|
74
|
+
git_commit_subject TEXT,
|
|
75
|
+
git_dirty INTEGER,
|
|
76
|
+
original_filename TEXT,
|
|
77
|
+
request_id TEXT,
|
|
78
|
+
has_inline_script INTEGER,
|
|
79
|
+
external_image_hosts TEXT,
|
|
80
|
+
ci_run_url TEXT,
|
|
81
|
+
ci_actor TEXT,
|
|
82
|
+
access_policy_id TEXT REFERENCES draft_access_policies(id),
|
|
83
|
+
UNIQUE (draft_id, version_number)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE upload_events (
|
|
87
|
+
id TEXT PRIMARY KEY,
|
|
88
|
+
draft_id TEXT NOT NULL REFERENCES drafts(id),
|
|
89
|
+
draft_version_id TEXT REFERENCES draft_versions(id),
|
|
90
|
+
api_key_id TEXT NOT NULL REFERENCES api_keys(id),
|
|
91
|
+
event_type TEXT NOT NULL,
|
|
92
|
+
source_ip TEXT,
|
|
93
|
+
user_agent TEXT,
|
|
94
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
95
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE identities (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
101
|
+
provider TEXT NOT NULL,
|
|
102
|
+
subject TEXT NOT NULL,
|
|
103
|
+
email TEXT,
|
|
104
|
+
email_verified INTEGER,
|
|
105
|
+
display_name TEXT,
|
|
106
|
+
picture_url TEXT,
|
|
107
|
+
pii_subject TEXT,
|
|
108
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
109
|
+
last_login_at TEXT,
|
|
110
|
+
UNIQUE (provider, subject)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE local_credentials (
|
|
114
|
+
account_id TEXT PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE,
|
|
115
|
+
username TEXT NOT NULL,
|
|
116
|
+
username_normalized TEXT NOT NULL UNIQUE,
|
|
117
|
+
password_algorithm TEXT NOT NULL,
|
|
118
|
+
password_salt TEXT NOT NULL,
|
|
119
|
+
password_hash TEXT NOT NULL,
|
|
120
|
+
password_parameters TEXT NOT NULL,
|
|
121
|
+
session_version INTEGER NOT NULL DEFAULT 1,
|
|
122
|
+
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
123
|
+
locked_until TEXT,
|
|
124
|
+
password_changed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
125
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
126
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
CREATE TABLE local_auth_events (
|
|
130
|
+
id TEXT PRIMARY KEY,
|
|
131
|
+
account_id TEXT REFERENCES accounts(id),
|
|
132
|
+
username_normalized TEXT,
|
|
133
|
+
event_type TEXT NOT NULL,
|
|
134
|
+
source_ip TEXT,
|
|
135
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
CREATE TABLE account_access_grants (
|
|
139
|
+
viewer_account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
140
|
+
resource_account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
141
|
+
created_by_api_key_id TEXT NOT NULL REFERENCES api_keys(id),
|
|
142
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
143
|
+
PRIMARY KEY (viewer_account_id, resource_account_id),
|
|
144
|
+
CHECK (viewer_account_id <> resource_account_id)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
CREATE TABLE access_policy_events (
|
|
148
|
+
id TEXT PRIMARY KEY,
|
|
149
|
+
draft_id TEXT NOT NULL REFERENCES drafts(id),
|
|
150
|
+
draft_version_id TEXT REFERENCES draft_versions(id),
|
|
151
|
+
actor_account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
152
|
+
actor_api_key_id TEXT REFERENCES api_keys(id),
|
|
153
|
+
previous_mode TEXT,
|
|
154
|
+
new_mode TEXT NOT NULL,
|
|
155
|
+
policy_id TEXT REFERENCES draft_access_policies(id),
|
|
156
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
CREATE TABLE site_settings (
|
|
160
|
+
key TEXT PRIMARY KEY,
|
|
161
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
|
162
|
+
updated_by_account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
163
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
CREATE TABLE admin_events (
|
|
167
|
+
id TEXT PRIMARY KEY,
|
|
168
|
+
actor_account_id TEXT NOT NULL REFERENCES accounts(id),
|
|
169
|
+
event_type TEXT NOT NULL,
|
|
170
|
+
setting_key TEXT NOT NULL,
|
|
171
|
+
previous_enabled INTEGER CHECK (previous_enabled IS NULL OR previous_enabled IN (0, 1)),
|
|
172
|
+
new_enabled INTEGER NOT NULL CHECK (new_enabled IN (0, 1)),
|
|
173
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
CREATE INDEX draft_versions_draft_id_idx ON draft_versions(draft_id);
|
|
177
|
+
CREATE INDEX upload_events_draft_id_idx ON upload_events(draft_id);
|
|
178
|
+
CREATE INDEX drafts_account_id_idx ON drafts(account_id);
|
|
179
|
+
CREATE INDEX account_access_grants_resource_idx ON account_access_grants(resource_account_id, viewer_account_id);
|
|
180
|
+
CREATE INDEX access_policy_events_draft_idx ON access_policy_events(draft_id, created_at DESC);
|
|
181
|
+
CREATE INDEX admin_events_setting_idx ON admin_events(setting_key, created_at DESC);
|
|
182
|
+
CREATE INDEX local_auth_events_account_idx ON local_auth_events(account_id, created_at DESC);
|
|
183
|
+
`
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
version: 2,
|
|
187
|
+
name: "durable-upload-state",
|
|
188
|
+
sql: `
|
|
189
|
+
ALTER TABLE draft_versions ADD COLUMN status TEXT NOT NULL DEFAULT 'ready'
|
|
190
|
+
CHECK (status IN ('pending', 'ready', 'failed'));
|
|
191
|
+
ALTER TABLE draft_versions ADD COLUMN failure_reason TEXT;
|
|
192
|
+
ALTER TABLE draft_versions ADD COLUMN finalized_at TEXT;
|
|
193
|
+
ALTER TABLE draft_versions ADD COLUMN draft_metadata_json TEXT;
|
|
194
|
+
CREATE INDEX draft_versions_status_idx ON draft_versions(status, created_at);
|
|
195
|
+
`
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
version: 3,
|
|
199
|
+
name: "access-policy-event-details",
|
|
200
|
+
sql: `
|
|
201
|
+
ALTER TABLE access_policy_events ADD COLUMN credential_revision INTEGER;
|
|
202
|
+
ALTER TABLE access_policy_events ADD COLUMN request_id TEXT;
|
|
203
|
+
`
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
version: 4,
|
|
207
|
+
name: "api-key-lineage",
|
|
208
|
+
sql: `
|
|
209
|
+
ALTER TABLE api_keys ADD COLUMN parent_api_key_id TEXT REFERENCES api_keys(id);
|
|
210
|
+
CREATE INDEX api_keys_parent_idx ON api_keys(parent_api_key_id);
|
|
211
|
+
`
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
//# sourceMappingURL=sqlite-migrations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-migrations.js","sourceRoot":"","sources":["../src/sqlite-migrations.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD;QACE,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,4BAA4B;QAClC,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAkLJ;KACF;IACD;QACE,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,sBAAsB;QAC5B,GAAG,EAAE;;;;;;;KAOJ;KACF;IACD;QACE,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,6BAA6B;QACnC,GAAG,EAAE;;;KAGJ;KACF;IACD;QACE,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,iBAAiB;QACvB,GAAG,EAAE;;;KAGJ;KACF;CACF,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DraftObjectStore {
|
|
2
|
+
putHtml(key: string, html: string): Promise<void>;
|
|
3
|
+
getHtml(key: string): Promise<string>;
|
|
4
|
+
exists(key: string): Promise<boolean>;
|
|
5
|
+
delete(key: string): Promise<void>;
|
|
6
|
+
probe(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare const draftObjectStore: DraftObjectStore;
|
|
9
|
+
export declare function assertStorageConfigured(): void;
|
|
10
|
+
export declare function putHtmlObject(key: string, html: string): Promise<void>;
|
|
11
|
+
export declare function getHtmlObject(key: string): Promise<string>;
|
|
12
|
+
export declare function objectExists(key: string): Promise<boolean>;
|
|
13
|
+
export declare function deleteHtmlObject(key: string): Promise<void>;
|
|
14
|
+
export declare function probeStorage(): Promise<void>;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { DeleteObjectCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { config, requireEnv } from "./config.js";
|
|
3
|
+
class S3DraftObjectStore {
|
|
4
|
+
client;
|
|
5
|
+
async putHtml(key, html) {
|
|
6
|
+
await this.getClient().send(new PutObjectCommand({
|
|
7
|
+
Bucket: bucketName(),
|
|
8
|
+
Key: key,
|
|
9
|
+
Body: html,
|
|
10
|
+
ContentType: "text/html; charset=utf-8",
|
|
11
|
+
CacheControl: "no-store"
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
async getHtml(key) {
|
|
15
|
+
const result = await this.getClient().send(new GetObjectCommand({
|
|
16
|
+
Bucket: bucketName(),
|
|
17
|
+
Key: key
|
|
18
|
+
}));
|
|
19
|
+
if (!result.Body)
|
|
20
|
+
throw new Error(`Stored HTML object has no body: ${key}`);
|
|
21
|
+
return result.Body.transformToString("utf-8");
|
|
22
|
+
}
|
|
23
|
+
async exists(key) {
|
|
24
|
+
try {
|
|
25
|
+
await this.getClient().send(new HeadObjectCommand({ Bucket: bucketName(), Key: key }));
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const status = error.$metadata?.httpStatusCode;
|
|
30
|
+
if (status === 404)
|
|
31
|
+
return false;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async delete(key) {
|
|
36
|
+
await this.getClient().send(new DeleteObjectCommand({ Bucket: bucketName(), Key: key }));
|
|
37
|
+
}
|
|
38
|
+
async probe() {
|
|
39
|
+
await this.getClient().send(new HeadBucketCommand({ Bucket: bucketName() }));
|
|
40
|
+
}
|
|
41
|
+
getClient() {
|
|
42
|
+
if (this.client)
|
|
43
|
+
return this.client;
|
|
44
|
+
this.client = new S3Client({
|
|
45
|
+
endpoint: requireEnv("AWS_ENDPOINT_URL", config.s3.endpoint),
|
|
46
|
+
region: requireEnv("AWS_DEFAULT_REGION", config.s3.region),
|
|
47
|
+
forcePathStyle: config.s3.forcePathStyle,
|
|
48
|
+
credentials: {
|
|
49
|
+
accessKeyId: requireEnv("AWS_ACCESS_KEY_ID", config.s3.accessKeyId),
|
|
50
|
+
secretAccessKey: requireEnv("AWS_SECRET_ACCESS_KEY", config.s3.secretAccessKey)
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return this.client;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const draftObjectStore = new S3DraftObjectStore();
|
|
57
|
+
export function assertStorageConfigured() {
|
|
58
|
+
requireEnv("AWS_ENDPOINT_URL", config.s3.endpoint);
|
|
59
|
+
requireEnv("AWS_ACCESS_KEY_ID", config.s3.accessKeyId);
|
|
60
|
+
requireEnv("AWS_SECRET_ACCESS_KEY", config.s3.secretAccessKey);
|
|
61
|
+
requireEnv("AWS_S3_BUCKET_NAME", config.s3.bucketName);
|
|
62
|
+
}
|
|
63
|
+
export async function putHtmlObject(key, html) {
|
|
64
|
+
assertStorageConfigured();
|
|
65
|
+
await draftObjectStore.putHtml(key, html);
|
|
66
|
+
}
|
|
67
|
+
export async function getHtmlObject(key) {
|
|
68
|
+
assertStorageConfigured();
|
|
69
|
+
return draftObjectStore.getHtml(key);
|
|
70
|
+
}
|
|
71
|
+
export async function objectExists(key) {
|
|
72
|
+
assertStorageConfigured();
|
|
73
|
+
return draftObjectStore.exists(key);
|
|
74
|
+
}
|
|
75
|
+
export async function deleteHtmlObject(key) {
|
|
76
|
+
assertStorageConfigured();
|
|
77
|
+
await draftObjectStore.delete(key);
|
|
78
|
+
}
|
|
79
|
+
export async function probeStorage() {
|
|
80
|
+
assertStorageConfigured();
|
|
81
|
+
await draftObjectStore.probe();
|
|
82
|
+
}
|
|
83
|
+
function bucketName() {
|
|
84
|
+
return requireEnv("AWS_S3_BUCKET_NAME", config.s3.bucketName);
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface AuthContext {
|
|
2
|
+
/** Internal credential snapshot; never serialize or log. */
|
|
3
|
+
readonly keyHash: string;
|
|
4
|
+
id: string;
|
|
5
|
+
account_id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
account_name: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SessionPayload {
|
|
10
|
+
accountId: string;
|
|
11
|
+
accountName?: string;
|
|
12
|
+
email?: string | null;
|
|
13
|
+
pictureUrl?: string | null;
|
|
14
|
+
exp: number;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
declare global {
|
|
18
|
+
namespace Express {
|
|
19
|
+
interface Request {
|
|
20
|
+
auth: AuthContext;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface PendingDraftMetadata {
|
|
2
|
+
title: string;
|
|
3
|
+
description: string | null;
|
|
4
|
+
repoOrg: string | null;
|
|
5
|
+
repoName: string | null;
|
|
6
|
+
repoHost: string | null;
|
|
7
|
+
eventType: "draft.created" | "draft.updated";
|
|
8
|
+
requestMetadata: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export declare function finalizeReservedVersion(versionId: string): Promise<boolean>;
|
|
11
|
+
export declare function recordUploadWriteError(versionId: string, error: unknown): Promise<void>;
|
|
12
|
+
export declare function reconcilePendingUploads({ olderThanMs, limit }?: {
|
|
13
|
+
olderThanMs?: number;
|
|
14
|
+
limit?: number;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
finalized: number;
|
|
17
|
+
failed: number;
|
|
18
|
+
pending: number;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { pool, withTransaction } from "./db.js";
|
|
2
|
+
import { newInternalId } from "./ids.js";
|
|
3
|
+
import { objectExists } from "./storage.js";
|
|
4
|
+
export async function finalizeReservedVersion(versionId) {
|
|
5
|
+
return withTransaction(async (client) => {
|
|
6
|
+
const result = await client.query("SELECT * FROM draft_versions WHERE id = $1 FOR UPDATE", [versionId]);
|
|
7
|
+
const version = result.rows[0];
|
|
8
|
+
if (!version || version.status === "failed")
|
|
9
|
+
return false;
|
|
10
|
+
if (version.status === "ready")
|
|
11
|
+
return true;
|
|
12
|
+
const metadata = parseMetadata(version.draft_metadata_json);
|
|
13
|
+
const current = await client.query(`
|
|
14
|
+
SELECT (
|
|
15
|
+
SELECT current_version.version_number
|
|
16
|
+
FROM draft_versions current_version
|
|
17
|
+
WHERE current_version.id = d.current_version_id
|
|
18
|
+
) AS version_number
|
|
19
|
+
FROM drafts d
|
|
20
|
+
WHERE d.id = $1
|
|
21
|
+
FOR UPDATE
|
|
22
|
+
LIMIT 1
|
|
23
|
+
`, [version.draft_id]);
|
|
24
|
+
const currentVersionNumber = current.rows[0]?.version_number;
|
|
25
|
+
const advancesCurrent = currentVersionNumber === null || currentVersionNumber === undefined
|
|
26
|
+
|| Number(currentVersionNumber) < Number(version.version_number);
|
|
27
|
+
await client.query(`
|
|
28
|
+
UPDATE draft_versions
|
|
29
|
+
SET status = 'ready', failure_reason = NULL, finalized_at = CURRENT_TIMESTAMP
|
|
30
|
+
WHERE id = $1
|
|
31
|
+
`, [version.id]);
|
|
32
|
+
if (advancesCurrent) {
|
|
33
|
+
await client.query(`
|
|
34
|
+
UPDATE drafts
|
|
35
|
+
SET current_version_id = $1,
|
|
36
|
+
title = $2,
|
|
37
|
+
description = COALESCE($3, description),
|
|
38
|
+
repo_org = COALESCE($4, repo_org),
|
|
39
|
+
repo_name = COALESCE($5, repo_name),
|
|
40
|
+
repo_host = COALESCE($6, repo_host),
|
|
41
|
+
updated_at = CURRENT_TIMESTAMP
|
|
42
|
+
WHERE id = $7
|
|
43
|
+
`, [
|
|
44
|
+
version.id,
|
|
45
|
+
metadata.title,
|
|
46
|
+
metadata.description,
|
|
47
|
+
metadata.repoOrg,
|
|
48
|
+
metadata.repoName,
|
|
49
|
+
metadata.repoHost,
|
|
50
|
+
version.draft_id
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
await client.query(`
|
|
54
|
+
INSERT INTO upload_events (
|
|
55
|
+
id, draft_id, draft_version_id, api_key_id, event_type,
|
|
56
|
+
source_ip, user_agent, metadata_json
|
|
57
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
58
|
+
`, [
|
|
59
|
+
newInternalId(),
|
|
60
|
+
version.draft_id,
|
|
61
|
+
version.id,
|
|
62
|
+
version.created_by_api_key_id,
|
|
63
|
+
metadata.eventType,
|
|
64
|
+
version.source_ip,
|
|
65
|
+
version.user_agent,
|
|
66
|
+
metadata.requestMetadata
|
|
67
|
+
]);
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export async function recordUploadWriteError(versionId, error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
await pool.query("UPDATE draft_versions SET failure_reason = $1 WHERE id = $2 AND status = 'pending'", [message.slice(0, 1000), versionId]);
|
|
74
|
+
}
|
|
75
|
+
export async function reconcilePendingUploads({ olderThanMs = 60_000, limit = 100 } = {}) {
|
|
76
|
+
const cutoff = new Date(Date.now() - olderThanMs).toISOString();
|
|
77
|
+
const result = await pool.query(`
|
|
78
|
+
SELECT * FROM draft_versions
|
|
79
|
+
WHERE status = 'pending' AND created_at < $1
|
|
80
|
+
ORDER BY created_at ASC
|
|
81
|
+
LIMIT $2
|
|
82
|
+
`, [cutoff, limit]);
|
|
83
|
+
let finalized = 0;
|
|
84
|
+
let failed = 0;
|
|
85
|
+
let pending = 0;
|
|
86
|
+
for (const version of result.rows) {
|
|
87
|
+
try {
|
|
88
|
+
if (await objectExists(version.object_key)) {
|
|
89
|
+
if (await finalizeReservedVersion(version.id))
|
|
90
|
+
finalized += 1;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
await pool.query(`
|
|
94
|
+
UPDATE draft_versions
|
|
95
|
+
SET status = 'failed',
|
|
96
|
+
failure_reason = COALESCE(failure_reason, 'Object was not found during reconciliation.'),
|
|
97
|
+
finalized_at = CURRENT_TIMESTAMP
|
|
98
|
+
WHERE id = $1 AND status = 'pending'
|
|
99
|
+
`, [version.id]);
|
|
100
|
+
failed += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
await recordUploadWriteError(version.id, error);
|
|
105
|
+
pending += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { finalized, failed, pending };
|
|
109
|
+
}
|
|
110
|
+
function parseMetadata(value) {
|
|
111
|
+
if (!value)
|
|
112
|
+
throw new Error("Pending draft metadata is missing.");
|
|
113
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
114
|
+
if (!parsed.title || !parsed.eventType)
|
|
115
|
+
throw new Error("Pending draft metadata is invalid.");
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=upload-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-state.js","sourceRoot":"","sources":["../src/upload-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Request } from "express";
|
|
2
|
+
import type { SessionPayload } from "./types.js";
|
|
3
|
+
export declare const SESSION_COOKIE = "openplan_session";
|
|
4
|
+
export declare const AUTH_STATE_COOKIE = "openplan_auth_state";
|
|
5
|
+
type SignedPayload = Record<string, unknown> & {
|
|
6
|
+
exp: number;
|
|
7
|
+
};
|
|
8
|
+
export interface AuthStatePayload extends SignedPayload {
|
|
9
|
+
state: string;
|
|
10
|
+
verifier: string;
|
|
11
|
+
next: string;
|
|
12
|
+
nonce?: string;
|
|
13
|
+
authProvider?: "shoo" | "oidc";
|
|
14
|
+
oidcBinding?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function signToken(payload: Record<string, unknown>, secret: string, ttlSeconds: number): string;
|
|
17
|
+
export declare function verifyToken(token: unknown, secret: string): SignedPayload | null;
|
|
18
|
+
export declare function createSessionCookie({ accountId, accountName, email, pictureUrl, authProvider, oidcBinding, sessionVersion }: {
|
|
19
|
+
accountId: string;
|
|
20
|
+
accountName: string;
|
|
21
|
+
email?: string | null;
|
|
22
|
+
pictureUrl?: string | null;
|
|
23
|
+
authProvider?: "shoo" | "local" | "oidc";
|
|
24
|
+
oidcBinding?: string;
|
|
25
|
+
sessionVersion?: number;
|
|
26
|
+
}): string;
|
|
27
|
+
export declare function clearSessionCookie(): string;
|
|
28
|
+
export declare function createAuthStateCookie(payload: Omit<AuthStatePayload, "exp">): string;
|
|
29
|
+
export declare function clearAuthStateCookie(): string;
|
|
30
|
+
export declare function readSession(req: Request): SessionPayload | null;
|
|
31
|
+
export declare function readAuthState(req: Request): AuthStatePayload | null;
|
|
32
|
+
export declare function readCookie(req: Request, name: string): string | null;
|
|
33
|
+
export {};
|