@smithers-orchestrator/control-plane 0.20.4
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/package.json +34 -0
- package/src/index.d.ts +152 -0
- package/src/index.js +1064 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 William Cory
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smithers-orchestrator/control-plane",
|
|
3
|
+
"version": "0.20.4",
|
|
4
|
+
"description": "Durable organization, project, billing, usage, secret-reference, and audit primitives for hosted Smithers control planes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.d.ts",
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./*": {
|
|
14
|
+
"types": "./src/index.d.ts",
|
|
15
|
+
"import": "./src/*.js",
|
|
16
|
+
"default": "./src/*.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src/"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@smithers-orchestrator/errors": "0.20.4"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/bun": "latest",
|
|
27
|
+
"typescript": "~5.9.3"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "bun test tests",
|
|
31
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
32
|
+
"build": "tsup --dts-only"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
type ControlPlaneSqlite = Database | {
|
|
4
|
+
exec(sql: string): unknown;
|
|
5
|
+
query(sql: string): {
|
|
6
|
+
run(...params: unknown[]): unknown;
|
|
7
|
+
get(...params: unknown[]): Record<string, unknown> | null;
|
|
8
|
+
all(...params: unknown[]): Array<Record<string, unknown>>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ControlPlaneOrg = {
|
|
13
|
+
orgId: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
name: string;
|
|
16
|
+
createdAtMs: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ControlPlaneTeam = {
|
|
20
|
+
orgId: string;
|
|
21
|
+
teamId: string;
|
|
22
|
+
slug: string;
|
|
23
|
+
name: string;
|
|
24
|
+
createdAtMs: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ControlPlaneProject = {
|
|
28
|
+
orgId: string;
|
|
29
|
+
projectId: string;
|
|
30
|
+
slug: string;
|
|
31
|
+
name: string;
|
|
32
|
+
metadata: Record<string, unknown>;
|
|
33
|
+
createdAtMs: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ControlPlaneBillingAccount = {
|
|
37
|
+
orgId: string;
|
|
38
|
+
plan: string;
|
|
39
|
+
billingCustomerId: string | null;
|
|
40
|
+
status: string;
|
|
41
|
+
updatedAtMs: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type ControlPlaneIdentityProvider = {
|
|
45
|
+
orgId: string;
|
|
46
|
+
providerId: string;
|
|
47
|
+
type: string;
|
|
48
|
+
issuer: string;
|
|
49
|
+
ssoUrl: string | null;
|
|
50
|
+
certificateRef: string | null;
|
|
51
|
+
status: string;
|
|
52
|
+
metadata: Record<string, unknown>;
|
|
53
|
+
createdAtMs: number;
|
|
54
|
+
updatedAtMs: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type ControlPlaneUsageEvent = {
|
|
58
|
+
id: number;
|
|
59
|
+
orgId: string;
|
|
60
|
+
projectId: string | null;
|
|
61
|
+
runId: string | null;
|
|
62
|
+
metric: string;
|
|
63
|
+
quantity: number;
|
|
64
|
+
unit: string;
|
|
65
|
+
observedAtMs: number;
|
|
66
|
+
metadata: Record<string, unknown>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type ControlPlaneUsageLimit = {
|
|
70
|
+
orgId: string;
|
|
71
|
+
projectId: string | null;
|
|
72
|
+
metric: string;
|
|
73
|
+
unit: string;
|
|
74
|
+
period: string;
|
|
75
|
+
limitQuantity: number;
|
|
76
|
+
updatedAtMs: number;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ControlPlaneUsageLimitCheck = ControlPlaneUsageLimit & {
|
|
80
|
+
usedQuantity: number;
|
|
81
|
+
remainingQuantity: number;
|
|
82
|
+
exceeded: boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type ControlPlaneUsageSummary = {
|
|
86
|
+
orgId: string;
|
|
87
|
+
metric: string;
|
|
88
|
+
unit: string;
|
|
89
|
+
quantity: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type ControlPlaneSecretRef = {
|
|
93
|
+
orgId: string;
|
|
94
|
+
projectId: string | null;
|
|
95
|
+
name: string;
|
|
96
|
+
provider: string;
|
|
97
|
+
ref: string;
|
|
98
|
+
createdBy: string | null;
|
|
99
|
+
createdAtMs: number;
|
|
100
|
+
rotatedAtMs: number | null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type ControlPlaneAuditEvent = {
|
|
104
|
+
id: number;
|
|
105
|
+
orgId: string;
|
|
106
|
+
projectId: string | null;
|
|
107
|
+
actorId: string | null;
|
|
108
|
+
action: string;
|
|
109
|
+
targetType: string;
|
|
110
|
+
targetId: string | null;
|
|
111
|
+
occurredAtMs: number;
|
|
112
|
+
metadata: Record<string, unknown>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
type ControlPlaneExport = {
|
|
116
|
+
exportedAtMs: number;
|
|
117
|
+
org: ControlPlaneOrg;
|
|
118
|
+
projects: ControlPlaneProject[];
|
|
119
|
+
teams: ControlPlaneTeam[];
|
|
120
|
+
billing: ControlPlaneBillingAccount | null;
|
|
121
|
+
identityProviders: ControlPlaneIdentityProvider[];
|
|
122
|
+
usage: ControlPlaneUsageSummary[];
|
|
123
|
+
usageLimits: ControlPlaneUsageLimit[];
|
|
124
|
+
secretRefs: ControlPlaneSecretRef[];
|
|
125
|
+
auditEvents: ControlPlaneAuditEvent[];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
declare function ensureControlPlaneTables(sqlite: ControlPlaneSqlite): void;
|
|
129
|
+
|
|
130
|
+
declare class ControlPlaneStore {
|
|
131
|
+
constructor(sqlite: ControlPlaneSqlite);
|
|
132
|
+
|
|
133
|
+
createOrg(input: { orgId?: string; slug: string; name: string; createdAtMs?: number }): ControlPlaneOrg;
|
|
134
|
+
getOrg(orgId: string): ControlPlaneOrg | null;
|
|
135
|
+
createTeam(input: { orgId: string; teamId?: string; slug: string; name: string; createdAtMs?: number }): ControlPlaneTeam;
|
|
136
|
+
addTeamMember(input: { orgId: string; teamId: string; userId: string; role?: string; createdAtMs?: number }): void;
|
|
137
|
+
createProject(input: { orgId: string; projectId?: string; slug: string; name: string; metadata?: Record<string, unknown>; createdAtMs?: number }): ControlPlaneProject;
|
|
138
|
+
addProjectTeam(input: { orgId: string; projectId: string; teamId: string; role?: string; createdAtMs?: number }): void;
|
|
139
|
+
upsertBillingAccount(input: { orgId: string; plan: string; billingCustomerId?: string | null; status?: string; updatedAtMs?: number }): ControlPlaneBillingAccount;
|
|
140
|
+
upsertIdentityProvider(input: { orgId: string; providerId?: string; type: string; issuer: string; ssoUrl?: string | null; certificateRef?: string | null; status?: string; metadata?: Record<string, unknown>; createdAtMs?: number; updatedAtMs?: number }): ControlPlaneIdentityProvider;
|
|
141
|
+
listIdentityProviders(input: { orgId: string; status?: string }): ControlPlaneIdentityProvider[];
|
|
142
|
+
recordUsage(input: { orgId: string; projectId?: string | null; runId?: string | null; metric: string; quantity: number; unit?: string; observedAtMs?: number; metadata?: Record<string, unknown> }): ControlPlaneUsageEvent;
|
|
143
|
+
summarizeUsage(input: { orgId: string; sinceMs?: number; untilMs?: number }): ControlPlaneUsageSummary[];
|
|
144
|
+
setUsageLimit(input: { orgId: string; projectId?: string | null; metric: string; unit?: string; period?: string; limitQuantity: number; updatedAtMs?: number }): ControlPlaneUsageLimit;
|
|
145
|
+
checkUsageLimit(input: { orgId: string; projectId?: string | null; metric: string; unit?: string; period?: string; sinceMs?: number; untilMs?: number }): ControlPlaneUsageLimitCheck | null;
|
|
146
|
+
putSecretRef(input: { orgId: string; projectId?: string | null; name: string; provider: string; ref: string; createdBy?: string | null; createdAtMs?: number; rotatedAtMs?: number | null }): ControlPlaneSecretRef;
|
|
147
|
+
listSecretRefs(input: { orgId: string; projectId?: string | null }): ControlPlaneSecretRef[];
|
|
148
|
+
recordAuditEvent(input: { orgId: string; projectId?: string | null; actorId?: string | null; action: string; targetType: string; targetId?: string | null; occurredAtMs?: number; metadata?: Record<string, unknown> }): ControlPlaneAuditEvent;
|
|
149
|
+
exportOrgAudit(input: { orgId: string; sinceMs?: number; untilMs?: number; exportedAtMs?: number }): ControlPlaneExport;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { type ControlPlaneAuditEvent, type ControlPlaneBillingAccount, type ControlPlaneExport, type ControlPlaneIdentityProvider, type ControlPlaneOrg, type ControlPlaneProject, type ControlPlaneSecretRef, type ControlPlaneSqlite, ControlPlaneStore, type ControlPlaneTeam, type ControlPlaneUsageEvent, type ControlPlaneUsageLimit, type ControlPlaneUsageLimitCheck, type ControlPlaneUsageSummary, ensureControlPlaneTables };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,1064 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import("./index.d.ts").ControlPlaneSqlite} ControlPlaneSqlite
|
|
6
|
+
* @typedef {import("./index.d.ts").ControlPlaneOrg} ControlPlaneOrg
|
|
7
|
+
* @typedef {import("./index.d.ts").ControlPlaneTeam} ControlPlaneTeam
|
|
8
|
+
* @typedef {import("./index.d.ts").ControlPlaneProject} ControlPlaneProject
|
|
9
|
+
* @typedef {import("./index.d.ts").ControlPlaneBillingAccount} ControlPlaneBillingAccount
|
|
10
|
+
* @typedef {import("./index.d.ts").ControlPlaneIdentityProvider} ControlPlaneIdentityProvider
|
|
11
|
+
* @typedef {import("./index.d.ts").ControlPlaneUsageEvent} ControlPlaneUsageEvent
|
|
12
|
+
* @typedef {import("./index.d.ts").ControlPlaneUsageLimit} ControlPlaneUsageLimit
|
|
13
|
+
* @typedef {import("./index.d.ts").ControlPlaneUsageLimitCheck} ControlPlaneUsageLimitCheck
|
|
14
|
+
* @typedef {import("./index.d.ts").ControlPlaneUsageSummary} ControlPlaneUsageSummary
|
|
15
|
+
* @typedef {import("./index.d.ts").ControlPlaneSecretRef} ControlPlaneSecretRef
|
|
16
|
+
* @typedef {import("./index.d.ts").ControlPlaneAuditEvent} ControlPlaneAuditEvent
|
|
17
|
+
* @typedef {import("./index.d.ts").ControlPlaneExport} ControlPlaneExport
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const SLUG_RE = /^(?:[a-z0-9]|[a-z0-9][a-z0-9-]{0,62}[a-z0-9])$/;
|
|
21
|
+
const ID_RE = /^[A-Za-z0-9:_-]{1,128}$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {ControlPlaneSqlite} sqlite
|
|
25
|
+
*/
|
|
26
|
+
export function ensureControlPlaneTables(sqlite) {
|
|
27
|
+
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
28
|
+
sqlite.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_orgs (
|
|
30
|
+
org_id TEXT PRIMARY KEY,
|
|
31
|
+
slug TEXT NOT NULL UNIQUE,
|
|
32
|
+
name TEXT NOT NULL,
|
|
33
|
+
created_at_ms INTEGER NOT NULL
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_teams (
|
|
37
|
+
org_id TEXT NOT NULL,
|
|
38
|
+
team_id TEXT NOT NULL,
|
|
39
|
+
slug TEXT NOT NULL,
|
|
40
|
+
name TEXT NOT NULL,
|
|
41
|
+
created_at_ms INTEGER NOT NULL,
|
|
42
|
+
PRIMARY KEY (org_id, team_id),
|
|
43
|
+
UNIQUE (org_id, slug),
|
|
44
|
+
FOREIGN KEY (org_id) REFERENCES _smithers_cp_orgs(org_id) ON DELETE CASCADE
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_team_members (
|
|
48
|
+
org_id TEXT NOT NULL,
|
|
49
|
+
team_id TEXT NOT NULL,
|
|
50
|
+
user_id TEXT NOT NULL,
|
|
51
|
+
role TEXT NOT NULL,
|
|
52
|
+
created_at_ms INTEGER NOT NULL,
|
|
53
|
+
PRIMARY KEY (org_id, team_id, user_id),
|
|
54
|
+
FOREIGN KEY (org_id, team_id) REFERENCES _smithers_cp_teams(org_id, team_id) ON DELETE CASCADE
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_projects (
|
|
58
|
+
org_id TEXT NOT NULL,
|
|
59
|
+
project_id TEXT NOT NULL,
|
|
60
|
+
slug TEXT NOT NULL,
|
|
61
|
+
name TEXT NOT NULL,
|
|
62
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
63
|
+
created_at_ms INTEGER NOT NULL,
|
|
64
|
+
PRIMARY KEY (org_id, project_id),
|
|
65
|
+
UNIQUE (org_id, slug),
|
|
66
|
+
FOREIGN KEY (org_id) REFERENCES _smithers_cp_orgs(org_id) ON DELETE CASCADE
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_project_teams (
|
|
70
|
+
org_id TEXT NOT NULL,
|
|
71
|
+
project_id TEXT NOT NULL,
|
|
72
|
+
team_id TEXT NOT NULL,
|
|
73
|
+
role TEXT NOT NULL,
|
|
74
|
+
created_at_ms INTEGER NOT NULL,
|
|
75
|
+
PRIMARY KEY (org_id, project_id, team_id),
|
|
76
|
+
FOREIGN KEY (org_id, project_id) REFERENCES _smithers_cp_projects(org_id, project_id) ON DELETE CASCADE,
|
|
77
|
+
FOREIGN KEY (org_id, team_id) REFERENCES _smithers_cp_teams(org_id, team_id) ON DELETE CASCADE
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_billing_accounts (
|
|
81
|
+
org_id TEXT PRIMARY KEY,
|
|
82
|
+
plan TEXT NOT NULL,
|
|
83
|
+
billing_customer_id TEXT,
|
|
84
|
+
status TEXT NOT NULL,
|
|
85
|
+
updated_at_ms INTEGER NOT NULL,
|
|
86
|
+
FOREIGN KEY (org_id) REFERENCES _smithers_cp_orgs(org_id) ON DELETE CASCADE
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_identity_providers (
|
|
90
|
+
org_id TEXT NOT NULL,
|
|
91
|
+
provider_id TEXT NOT NULL,
|
|
92
|
+
type TEXT NOT NULL,
|
|
93
|
+
issuer TEXT NOT NULL,
|
|
94
|
+
sso_url TEXT,
|
|
95
|
+
certificate_ref TEXT,
|
|
96
|
+
status TEXT NOT NULL,
|
|
97
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
98
|
+
created_at_ms INTEGER NOT NULL,
|
|
99
|
+
updated_at_ms INTEGER NOT NULL,
|
|
100
|
+
PRIMARY KEY (org_id, provider_id),
|
|
101
|
+
FOREIGN KEY (org_id) REFERENCES _smithers_cp_orgs(org_id) ON DELETE CASCADE
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE INDEX IF NOT EXISTS _smithers_cp_idp_org_status_idx
|
|
105
|
+
ON _smithers_cp_identity_providers(org_id, status);
|
|
106
|
+
|
|
107
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_usage_events (
|
|
108
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
109
|
+
org_id TEXT NOT NULL,
|
|
110
|
+
project_id TEXT,
|
|
111
|
+
run_id TEXT,
|
|
112
|
+
metric TEXT NOT NULL,
|
|
113
|
+
quantity REAL NOT NULL,
|
|
114
|
+
unit TEXT NOT NULL,
|
|
115
|
+
observed_at_ms INTEGER NOT NULL,
|
|
116
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
117
|
+
FOREIGN KEY (org_id) REFERENCES _smithers_cp_orgs(org_id) ON DELETE CASCADE,
|
|
118
|
+
FOREIGN KEY (org_id, project_id) REFERENCES _smithers_cp_projects(org_id, project_id) ON DELETE CASCADE
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE INDEX IF NOT EXISTS _smithers_cp_usage_org_time_idx
|
|
122
|
+
ON _smithers_cp_usage_events(org_id, observed_at_ms);
|
|
123
|
+
|
|
124
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_usage_limits (
|
|
125
|
+
org_id TEXT NOT NULL,
|
|
126
|
+
project_key TEXT NOT NULL,
|
|
127
|
+
project_id TEXT,
|
|
128
|
+
metric TEXT NOT NULL,
|
|
129
|
+
unit TEXT NOT NULL,
|
|
130
|
+
period TEXT NOT NULL,
|
|
131
|
+
limit_quantity REAL NOT NULL,
|
|
132
|
+
updated_at_ms INTEGER NOT NULL,
|
|
133
|
+
PRIMARY KEY (org_id, project_key, metric, unit, period),
|
|
134
|
+
FOREIGN KEY (org_id) REFERENCES _smithers_cp_orgs(org_id) ON DELETE CASCADE
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
CREATE INDEX IF NOT EXISTS _smithers_cp_usage_limits_org_idx
|
|
138
|
+
ON _smithers_cp_usage_limits(org_id, metric, unit, period);
|
|
139
|
+
|
|
140
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_secret_refs (
|
|
141
|
+
org_id TEXT NOT NULL,
|
|
142
|
+
project_key TEXT NOT NULL,
|
|
143
|
+
project_id TEXT,
|
|
144
|
+
name TEXT NOT NULL,
|
|
145
|
+
provider TEXT NOT NULL,
|
|
146
|
+
ref TEXT NOT NULL,
|
|
147
|
+
created_by TEXT,
|
|
148
|
+
created_at_ms INTEGER NOT NULL,
|
|
149
|
+
rotated_at_ms INTEGER,
|
|
150
|
+
PRIMARY KEY (org_id, project_key, name),
|
|
151
|
+
FOREIGN KEY (org_id) REFERENCES _smithers_cp_orgs(org_id) ON DELETE CASCADE,
|
|
152
|
+
FOREIGN KEY (org_id, project_id) REFERENCES _smithers_cp_projects(org_id, project_id) ON DELETE CASCADE
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_audit_events (
|
|
156
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
157
|
+
org_id TEXT NOT NULL,
|
|
158
|
+
project_id TEXT,
|
|
159
|
+
actor_id TEXT,
|
|
160
|
+
action TEXT NOT NULL,
|
|
161
|
+
target_type TEXT NOT NULL,
|
|
162
|
+
target_id TEXT,
|
|
163
|
+
occurred_at_ms INTEGER NOT NULL,
|
|
164
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
165
|
+
FOREIGN KEY (org_id) REFERENCES _smithers_cp_orgs(org_id) ON DELETE CASCADE,
|
|
166
|
+
FOREIGN KEY (org_id, project_id) REFERENCES _smithers_cp_projects(org_id, project_id) ON DELETE CASCADE
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
CREATE INDEX IF NOT EXISTS _smithers_cp_audit_org_time_idx
|
|
170
|
+
ON _smithers_cp_audit_events(org_id, occurred_at_ms);
|
|
171
|
+
`);
|
|
172
|
+
migrateSecretRefsProjectKey(sqlite);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {ControlPlaneSqlite} sqlite
|
|
177
|
+
*/
|
|
178
|
+
function migrateSecretRefsProjectKey(sqlite) {
|
|
179
|
+
const columns = sqlite.query("PRAGMA table_info(_smithers_cp_secret_refs)").all();
|
|
180
|
+
if (columns.some((column) => String(column.name) === "project_key")) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
sqlite.exec(`
|
|
184
|
+
ALTER TABLE _smithers_cp_secret_refs RENAME TO _smithers_cp_secret_refs_legacy;
|
|
185
|
+
|
|
186
|
+
CREATE TABLE _smithers_cp_secret_refs (
|
|
187
|
+
org_id TEXT NOT NULL,
|
|
188
|
+
project_key TEXT NOT NULL,
|
|
189
|
+
project_id TEXT,
|
|
190
|
+
name TEXT NOT NULL,
|
|
191
|
+
provider TEXT NOT NULL,
|
|
192
|
+
ref TEXT NOT NULL,
|
|
193
|
+
created_by TEXT,
|
|
194
|
+
created_at_ms INTEGER NOT NULL,
|
|
195
|
+
rotated_at_ms INTEGER,
|
|
196
|
+
PRIMARY KEY (org_id, project_key, name),
|
|
197
|
+
FOREIGN KEY (org_id) REFERENCES _smithers_cp_orgs(org_id) ON DELETE CASCADE,
|
|
198
|
+
FOREIGN KEY (org_id, project_id) REFERENCES _smithers_cp_projects(org_id, project_id) ON DELETE CASCADE
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
INSERT OR REPLACE INTO _smithers_cp_secret_refs (
|
|
202
|
+
org_id, project_key, project_id, name, provider, ref, created_by, created_at_ms, rotated_at_ms
|
|
203
|
+
)
|
|
204
|
+
SELECT
|
|
205
|
+
org_id,
|
|
206
|
+
COALESCE(project_id, '__org__') AS project_key,
|
|
207
|
+
project_id,
|
|
208
|
+
name,
|
|
209
|
+
provider,
|
|
210
|
+
ref,
|
|
211
|
+
created_by,
|
|
212
|
+
created_at_ms,
|
|
213
|
+
rotated_at_ms
|
|
214
|
+
FROM _smithers_cp_secret_refs_legacy
|
|
215
|
+
ORDER BY created_at_ms;
|
|
216
|
+
|
|
217
|
+
DROP TABLE _smithers_cp_secret_refs_legacy;
|
|
218
|
+
`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {string} field
|
|
223
|
+
* @param {unknown} value
|
|
224
|
+
*/
|
|
225
|
+
function nonEmptyString(field, value) {
|
|
226
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
227
|
+
throw new SmithersError("INVALID_INPUT", `${field} is required.`);
|
|
228
|
+
}
|
|
229
|
+
return value.trim();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {string} field
|
|
234
|
+
* @param {unknown} value
|
|
235
|
+
*/
|
|
236
|
+
function optionalId(field, value) {
|
|
237
|
+
const id = typeof value === "string" && value.trim() ? value.trim() : randomUUID();
|
|
238
|
+
if (!ID_RE.test(id)) {
|
|
239
|
+
throw new SmithersError("INVALID_INPUT", `${field} must match ${ID_RE}.`, { field });
|
|
240
|
+
}
|
|
241
|
+
return id;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {string} field
|
|
246
|
+
* @param {unknown} value
|
|
247
|
+
*/
|
|
248
|
+
function requiredId(field, value) {
|
|
249
|
+
const id = nonEmptyString(field, value);
|
|
250
|
+
if (!ID_RE.test(id)) {
|
|
251
|
+
throw new SmithersError("INVALID_INPUT", `${field} must match ${ID_RE}.`, { field });
|
|
252
|
+
}
|
|
253
|
+
return id;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {string} field
|
|
258
|
+
* @param {unknown} value
|
|
259
|
+
*/
|
|
260
|
+
function slug(field, value) {
|
|
261
|
+
const out = nonEmptyString(field, value);
|
|
262
|
+
if (!SLUG_RE.test(out)) {
|
|
263
|
+
throw new SmithersError("INVALID_INPUT", `${field} must be a lowercase slug.`, { field });
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @param {unknown} value
|
|
270
|
+
*/
|
|
271
|
+
function jsonObject(value) {
|
|
272
|
+
if (value === undefined) {
|
|
273
|
+
return {};
|
|
274
|
+
}
|
|
275
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
276
|
+
throw new SmithersError("INVALID_INPUT", "metadata must be a JSON object.");
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {unknown} value
|
|
283
|
+
*/
|
|
284
|
+
function parseJsonObject(value) {
|
|
285
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
286
|
+
return {};
|
|
287
|
+
}
|
|
288
|
+
const parsed = JSON.parse(value);
|
|
289
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @param {unknown} value
|
|
294
|
+
*/
|
|
295
|
+
function timestamp(value) {
|
|
296
|
+
const n = value === undefined ? Date.now() : Number(value);
|
|
297
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
298
|
+
throw new SmithersError("INVALID_INPUT", "timestamp must be a non-negative finite number.");
|
|
299
|
+
}
|
|
300
|
+
return Math.floor(n);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {unknown} value
|
|
305
|
+
*/
|
|
306
|
+
function quantity(value) {
|
|
307
|
+
const n = Number(value);
|
|
308
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
309
|
+
throw new SmithersError("INVALID_INPUT", "quantity must be a non-negative finite number.");
|
|
310
|
+
}
|
|
311
|
+
return n;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @param {string | null} projectId
|
|
316
|
+
*/
|
|
317
|
+
function projectKey(projectId) {
|
|
318
|
+
return projectId ?? "__org__";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @param {Record<string, unknown>} row
|
|
323
|
+
* @returns {ControlPlaneOrg}
|
|
324
|
+
*/
|
|
325
|
+
function orgRow(row) {
|
|
326
|
+
return {
|
|
327
|
+
orgId: String(row.orgId),
|
|
328
|
+
slug: String(row.slug),
|
|
329
|
+
name: String(row.name),
|
|
330
|
+
createdAtMs: Number(row.createdAtMs),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @param {Record<string, unknown>} row
|
|
336
|
+
* @returns {ControlPlaneTeam}
|
|
337
|
+
*/
|
|
338
|
+
function teamRow(row) {
|
|
339
|
+
return {
|
|
340
|
+
orgId: String(row.orgId),
|
|
341
|
+
teamId: String(row.teamId),
|
|
342
|
+
slug: String(row.slug),
|
|
343
|
+
name: String(row.name),
|
|
344
|
+
createdAtMs: Number(row.createdAtMs),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {Record<string, unknown>} row
|
|
350
|
+
* @returns {ControlPlaneProject}
|
|
351
|
+
*/
|
|
352
|
+
function projectRow(row) {
|
|
353
|
+
return {
|
|
354
|
+
orgId: String(row.orgId),
|
|
355
|
+
projectId: String(row.projectId),
|
|
356
|
+
slug: String(row.slug),
|
|
357
|
+
name: String(row.name),
|
|
358
|
+
metadata: parseJsonObject(row.metadataJson),
|
|
359
|
+
createdAtMs: Number(row.createdAtMs),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* @param {Record<string, unknown>} row
|
|
365
|
+
* @returns {ControlPlaneBillingAccount}
|
|
366
|
+
*/
|
|
367
|
+
function billingRow(row) {
|
|
368
|
+
return {
|
|
369
|
+
orgId: String(row.orgId),
|
|
370
|
+
plan: String(row.plan),
|
|
371
|
+
billingCustomerId: row.billingCustomerId === null ? null : String(row.billingCustomerId),
|
|
372
|
+
status: String(row.status),
|
|
373
|
+
updatedAtMs: Number(row.updatedAtMs),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* @param {Record<string, unknown>} row
|
|
379
|
+
* @returns {ControlPlaneIdentityProvider}
|
|
380
|
+
*/
|
|
381
|
+
function identityProviderRow(row) {
|
|
382
|
+
return {
|
|
383
|
+
orgId: String(row.orgId),
|
|
384
|
+
providerId: String(row.providerId),
|
|
385
|
+
type: String(row.type),
|
|
386
|
+
issuer: String(row.issuer),
|
|
387
|
+
ssoUrl: row.ssoUrl === null ? null : String(row.ssoUrl),
|
|
388
|
+
certificateRef: row.certificateRef === null ? null : String(row.certificateRef),
|
|
389
|
+
status: String(row.status),
|
|
390
|
+
metadata: parseJsonObject(row.metadataJson),
|
|
391
|
+
createdAtMs: Number(row.createdAtMs),
|
|
392
|
+
updatedAtMs: Number(row.updatedAtMs),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* @param {Record<string, unknown>} row
|
|
398
|
+
* @returns {ControlPlaneUsageEvent}
|
|
399
|
+
*/
|
|
400
|
+
function usageRow(row) {
|
|
401
|
+
return {
|
|
402
|
+
id: Number(row.id),
|
|
403
|
+
orgId: String(row.orgId),
|
|
404
|
+
projectId: row.projectId === null ? null : String(row.projectId),
|
|
405
|
+
runId: row.runId === null ? null : String(row.runId),
|
|
406
|
+
metric: String(row.metric),
|
|
407
|
+
quantity: Number(row.quantity),
|
|
408
|
+
unit: String(row.unit),
|
|
409
|
+
observedAtMs: Number(row.observedAtMs),
|
|
410
|
+
metadata: parseJsonObject(row.metadataJson),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* @param {Record<string, unknown>} row
|
|
416
|
+
* @returns {ControlPlaneUsageLimit}
|
|
417
|
+
*/
|
|
418
|
+
function usageLimitRow(row) {
|
|
419
|
+
return {
|
|
420
|
+
orgId: String(row.orgId),
|
|
421
|
+
projectId: row.projectId === null ? null : String(row.projectId),
|
|
422
|
+
metric: String(row.metric),
|
|
423
|
+
unit: String(row.unit),
|
|
424
|
+
period: String(row.period),
|
|
425
|
+
limitQuantity: Number(row.limitQuantity),
|
|
426
|
+
updatedAtMs: Number(row.updatedAtMs),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @param {Record<string, unknown>} row
|
|
432
|
+
* @returns {ControlPlaneSecretRef}
|
|
433
|
+
*/
|
|
434
|
+
function secretRefRow(row) {
|
|
435
|
+
return {
|
|
436
|
+
orgId: String(row.orgId),
|
|
437
|
+
projectId: row.projectId === null ? null : String(row.projectId),
|
|
438
|
+
name: String(row.name),
|
|
439
|
+
provider: String(row.provider),
|
|
440
|
+
ref: String(row.ref),
|
|
441
|
+
createdBy: row.createdBy === null ? null : String(row.createdBy),
|
|
442
|
+
createdAtMs: Number(row.createdAtMs),
|
|
443
|
+
rotatedAtMs: row.rotatedAtMs === null ? null : Number(row.rotatedAtMs),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @param {Record<string, unknown>} row
|
|
449
|
+
* @returns {ControlPlaneAuditEvent}
|
|
450
|
+
*/
|
|
451
|
+
function auditRow(row) {
|
|
452
|
+
return {
|
|
453
|
+
id: Number(row.id),
|
|
454
|
+
orgId: String(row.orgId),
|
|
455
|
+
projectId: row.projectId === null ? null : String(row.projectId),
|
|
456
|
+
actorId: row.actorId === null ? null : String(row.actorId),
|
|
457
|
+
action: String(row.action),
|
|
458
|
+
targetType: String(row.targetType),
|
|
459
|
+
targetId: row.targetId === null ? null : String(row.targetId),
|
|
460
|
+
occurredAtMs: Number(row.occurredAtMs),
|
|
461
|
+
metadata: parseJsonObject(row.metadataJson),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @param {ControlPlaneSqlite} sqlite
|
|
467
|
+
* @param {string} orgId
|
|
468
|
+
* @param {string} projectId
|
|
469
|
+
*/
|
|
470
|
+
function assertProjectExists(sqlite, orgId, projectId) {
|
|
471
|
+
const row = sqlite.query(`
|
|
472
|
+
SELECT 1 AS ok
|
|
473
|
+
FROM _smithers_cp_projects
|
|
474
|
+
WHERE org_id = ? AND project_id = ?
|
|
475
|
+
LIMIT 1
|
|
476
|
+
`).get(orgId, projectId);
|
|
477
|
+
if (!row) {
|
|
478
|
+
throw new SmithersError("INVALID_INPUT", `Control-plane project not found: ${projectId}`, { orgId, projectId });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export class ControlPlaneStore {
|
|
483
|
+
/** @type {ControlPlaneSqlite} */
|
|
484
|
+
sqlite;
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* @param {ControlPlaneSqlite} sqlite
|
|
488
|
+
*/
|
|
489
|
+
constructor(sqlite) {
|
|
490
|
+
this.sqlite = sqlite;
|
|
491
|
+
ensureControlPlaneTables(sqlite);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* @param {{ orgId?: string; slug: string; name: string; createdAtMs?: number }} input
|
|
496
|
+
* @returns {ControlPlaneOrg}
|
|
497
|
+
*/
|
|
498
|
+
createOrg(input) {
|
|
499
|
+
const orgId = optionalId("orgId", input.orgId);
|
|
500
|
+
const createdAtMs = timestamp(input.createdAtMs);
|
|
501
|
+
this.sqlite.query(`
|
|
502
|
+
INSERT INTO _smithers_cp_orgs (org_id, slug, name, created_at_ms)
|
|
503
|
+
VALUES (?, ?, ?, ?)
|
|
504
|
+
`).run(orgId, slug("slug", input.slug), nonEmptyString("name", input.name), createdAtMs);
|
|
505
|
+
this.recordAuditEvent({
|
|
506
|
+
orgId,
|
|
507
|
+
actorId: "system",
|
|
508
|
+
action: "org.create",
|
|
509
|
+
targetType: "org",
|
|
510
|
+
targetId: orgId,
|
|
511
|
+
occurredAtMs: createdAtMs,
|
|
512
|
+
});
|
|
513
|
+
return this.getOrg(orgId);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* @param {string} orgId
|
|
518
|
+
* @returns {ControlPlaneOrg | null}
|
|
519
|
+
*/
|
|
520
|
+
getOrg(orgId) {
|
|
521
|
+
const row = this.sqlite.query(`
|
|
522
|
+
SELECT org_id AS orgId, slug, name, created_at_ms AS createdAtMs
|
|
523
|
+
FROM _smithers_cp_orgs
|
|
524
|
+
WHERE org_id = ?
|
|
525
|
+
LIMIT 1
|
|
526
|
+
`).get(requiredId("orgId", orgId));
|
|
527
|
+
return row ? orgRow(row) : null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* @param {{ orgId: string; teamId?: string; slug: string; name: string; createdAtMs?: number }} input
|
|
532
|
+
* @returns {ControlPlaneTeam}
|
|
533
|
+
*/
|
|
534
|
+
createTeam(input) {
|
|
535
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
536
|
+
const teamId = optionalId("teamId", input.teamId);
|
|
537
|
+
const createdAtMs = timestamp(input.createdAtMs);
|
|
538
|
+
this.sqlite.query(`
|
|
539
|
+
INSERT INTO _smithers_cp_teams (org_id, team_id, slug, name, created_at_ms)
|
|
540
|
+
VALUES (?, ?, ?, ?, ?)
|
|
541
|
+
`).run(orgId, teamId, slug("slug", input.slug), nonEmptyString("name", input.name), createdAtMs);
|
|
542
|
+
this.recordAuditEvent({
|
|
543
|
+
orgId,
|
|
544
|
+
action: "team.create",
|
|
545
|
+
targetType: "team",
|
|
546
|
+
targetId: teamId,
|
|
547
|
+
occurredAtMs: createdAtMs,
|
|
548
|
+
});
|
|
549
|
+
const row = this.sqlite.query(`
|
|
550
|
+
SELECT org_id AS orgId, team_id AS teamId, slug, name, created_at_ms AS createdAtMs
|
|
551
|
+
FROM _smithers_cp_teams
|
|
552
|
+
WHERE org_id = ? AND team_id = ?
|
|
553
|
+
LIMIT 1
|
|
554
|
+
`).get(orgId, teamId);
|
|
555
|
+
return teamRow(row);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* @param {{ orgId: string; teamId: string; userId: string; role?: string; createdAtMs?: number }} input
|
|
560
|
+
*/
|
|
561
|
+
addTeamMember(input) {
|
|
562
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
563
|
+
const teamId = requiredId("teamId", input.teamId);
|
|
564
|
+
const userId = requiredId("userId", input.userId);
|
|
565
|
+
const role = nonEmptyString("role", input.role ?? "member");
|
|
566
|
+
const createdAtMs = timestamp(input.createdAtMs);
|
|
567
|
+
this.sqlite.query(`
|
|
568
|
+
INSERT INTO _smithers_cp_team_members (org_id, team_id, user_id, role, created_at_ms)
|
|
569
|
+
VALUES (?, ?, ?, ?, ?)
|
|
570
|
+
ON CONFLICT(org_id, team_id, user_id) DO UPDATE SET role = excluded.role
|
|
571
|
+
`).run(orgId, teamId, userId, role, createdAtMs);
|
|
572
|
+
this.recordAuditEvent({
|
|
573
|
+
orgId,
|
|
574
|
+
actorId: userId,
|
|
575
|
+
action: "team.member.upsert",
|
|
576
|
+
targetType: "team",
|
|
577
|
+
targetId: teamId,
|
|
578
|
+
occurredAtMs: createdAtMs,
|
|
579
|
+
metadata: { role },
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* @param {{ orgId: string; projectId?: string; slug: string; name: string; metadata?: Record<string, unknown>; createdAtMs?: number }} input
|
|
585
|
+
* @returns {ControlPlaneProject}
|
|
586
|
+
*/
|
|
587
|
+
createProject(input) {
|
|
588
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
589
|
+
const projectId = optionalId("projectId", input.projectId);
|
|
590
|
+
const metadata = jsonObject(input.metadata);
|
|
591
|
+
const createdAtMs = timestamp(input.createdAtMs);
|
|
592
|
+
this.sqlite.query(`
|
|
593
|
+
INSERT INTO _smithers_cp_projects (org_id, project_id, slug, name, metadata_json, created_at_ms)
|
|
594
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
595
|
+
`).run(orgId, projectId, slug("slug", input.slug), nonEmptyString("name", input.name), JSON.stringify(metadata), createdAtMs);
|
|
596
|
+
this.recordAuditEvent({
|
|
597
|
+
orgId,
|
|
598
|
+
projectId,
|
|
599
|
+
action: "project.create",
|
|
600
|
+
targetType: "project",
|
|
601
|
+
targetId: projectId,
|
|
602
|
+
occurredAtMs: createdAtMs,
|
|
603
|
+
});
|
|
604
|
+
const row = this.sqlite.query(`
|
|
605
|
+
SELECT org_id AS orgId, project_id AS projectId, slug, name, metadata_json AS metadataJson, created_at_ms AS createdAtMs
|
|
606
|
+
FROM _smithers_cp_projects
|
|
607
|
+
WHERE org_id = ? AND project_id = ?
|
|
608
|
+
LIMIT 1
|
|
609
|
+
`).get(orgId, projectId);
|
|
610
|
+
return projectRow(row);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* @param {{ orgId: string; projectId: string; teamId: string; role?: string; createdAtMs?: number }} input
|
|
615
|
+
*/
|
|
616
|
+
addProjectTeam(input) {
|
|
617
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
618
|
+
const projectId = requiredId("projectId", input.projectId);
|
|
619
|
+
const teamId = requiredId("teamId", input.teamId);
|
|
620
|
+
const role = nonEmptyString("role", input.role ?? "viewer");
|
|
621
|
+
const createdAtMs = timestamp(input.createdAtMs);
|
|
622
|
+
this.sqlite.query(`
|
|
623
|
+
INSERT INTO _smithers_cp_project_teams (org_id, project_id, team_id, role, created_at_ms)
|
|
624
|
+
VALUES (?, ?, ?, ?, ?)
|
|
625
|
+
ON CONFLICT(org_id, project_id, team_id) DO UPDATE SET role = excluded.role
|
|
626
|
+
`).run(orgId, projectId, teamId, role, createdAtMs);
|
|
627
|
+
this.recordAuditEvent({
|
|
628
|
+
orgId,
|
|
629
|
+
projectId,
|
|
630
|
+
action: "project.team.upsert",
|
|
631
|
+
targetType: "team",
|
|
632
|
+
targetId: teamId,
|
|
633
|
+
occurredAtMs: createdAtMs,
|
|
634
|
+
metadata: { role },
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* @param {{ orgId: string; plan: string; billingCustomerId?: string | null; status?: string; updatedAtMs?: number }} input
|
|
640
|
+
* @returns {ControlPlaneBillingAccount}
|
|
641
|
+
*/
|
|
642
|
+
upsertBillingAccount(input) {
|
|
643
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
644
|
+
const updatedAtMs = timestamp(input.updatedAtMs);
|
|
645
|
+
const status = nonEmptyString("status", input.status ?? "active");
|
|
646
|
+
this.sqlite.query(`
|
|
647
|
+
INSERT INTO _smithers_cp_billing_accounts (org_id, plan, billing_customer_id, status, updated_at_ms)
|
|
648
|
+
VALUES (?, ?, ?, ?, ?)
|
|
649
|
+
ON CONFLICT(org_id) DO UPDATE SET
|
|
650
|
+
plan = excluded.plan,
|
|
651
|
+
billing_customer_id = excluded.billing_customer_id,
|
|
652
|
+
status = excluded.status,
|
|
653
|
+
updated_at_ms = excluded.updated_at_ms
|
|
654
|
+
`).run(orgId, nonEmptyString("plan", input.plan), input.billingCustomerId ?? null, status, updatedAtMs);
|
|
655
|
+
this.recordAuditEvent({
|
|
656
|
+
orgId,
|
|
657
|
+
action: "billing.account.upsert",
|
|
658
|
+
targetType: "billing_account",
|
|
659
|
+
targetId: orgId,
|
|
660
|
+
occurredAtMs: updatedAtMs,
|
|
661
|
+
metadata: { plan: input.plan, status },
|
|
662
|
+
});
|
|
663
|
+
const row = this.sqlite.query(`
|
|
664
|
+
SELECT org_id AS orgId, plan, billing_customer_id AS billingCustomerId, status, updated_at_ms AS updatedAtMs
|
|
665
|
+
FROM _smithers_cp_billing_accounts
|
|
666
|
+
WHERE org_id = ?
|
|
667
|
+
LIMIT 1
|
|
668
|
+
`).get(orgId);
|
|
669
|
+
return billingRow(row);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* @param {{ orgId: string; providerId?: string; type: "saml" | "oidc" | string; issuer: string; ssoUrl?: string | null; certificateRef?: string | null; status?: string; metadata?: Record<string, unknown>; createdAtMs?: number; updatedAtMs?: number }} input
|
|
674
|
+
* @returns {ControlPlaneIdentityProvider}
|
|
675
|
+
*/
|
|
676
|
+
upsertIdentityProvider(input) {
|
|
677
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
678
|
+
const providerId = optionalId("providerId", input.providerId);
|
|
679
|
+
const metadata = jsonObject(input.metadata);
|
|
680
|
+
const updatedAtMs = timestamp(input.updatedAtMs);
|
|
681
|
+
const createdAtMs = timestamp(input.createdAtMs ?? updatedAtMs);
|
|
682
|
+
const status = nonEmptyString("status", input.status ?? "active");
|
|
683
|
+
this.sqlite.query(`
|
|
684
|
+
INSERT INTO _smithers_cp_identity_providers (
|
|
685
|
+
org_id, provider_id, type, issuer, sso_url, certificate_ref, status, metadata_json, created_at_ms, updated_at_ms
|
|
686
|
+
)
|
|
687
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
688
|
+
ON CONFLICT(org_id, provider_id) DO UPDATE SET
|
|
689
|
+
type = excluded.type,
|
|
690
|
+
issuer = excluded.issuer,
|
|
691
|
+
sso_url = excluded.sso_url,
|
|
692
|
+
certificate_ref = excluded.certificate_ref,
|
|
693
|
+
status = excluded.status,
|
|
694
|
+
metadata_json = excluded.metadata_json,
|
|
695
|
+
updated_at_ms = excluded.updated_at_ms
|
|
696
|
+
`).run(
|
|
697
|
+
orgId,
|
|
698
|
+
providerId,
|
|
699
|
+
nonEmptyString("type", input.type),
|
|
700
|
+
nonEmptyString("issuer", input.issuer),
|
|
701
|
+
input.ssoUrl ? nonEmptyString("ssoUrl", input.ssoUrl) : null,
|
|
702
|
+
input.certificateRef ? nonEmptyString("certificateRef", input.certificateRef) : null,
|
|
703
|
+
status,
|
|
704
|
+
JSON.stringify(metadata),
|
|
705
|
+
createdAtMs,
|
|
706
|
+
updatedAtMs,
|
|
707
|
+
);
|
|
708
|
+
this.recordAuditEvent({
|
|
709
|
+
orgId,
|
|
710
|
+
action: "identity_provider.upsert",
|
|
711
|
+
targetType: "identity_provider",
|
|
712
|
+
targetId: providerId,
|
|
713
|
+
occurredAtMs: updatedAtMs,
|
|
714
|
+
metadata: { type: input.type, status },
|
|
715
|
+
});
|
|
716
|
+
const row = this.sqlite.query(`
|
|
717
|
+
SELECT org_id AS orgId, provider_id AS providerId, type, issuer, sso_url AS ssoUrl, certificate_ref AS certificateRef, status, metadata_json AS metadataJson, created_at_ms AS createdAtMs, updated_at_ms AS updatedAtMs
|
|
718
|
+
FROM _smithers_cp_identity_providers
|
|
719
|
+
WHERE org_id = ? AND provider_id = ?
|
|
720
|
+
LIMIT 1
|
|
721
|
+
`).get(orgId, providerId);
|
|
722
|
+
return identityProviderRow(row);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* @param {{ orgId: string; status?: string }} input
|
|
727
|
+
* @returns {ControlPlaneIdentityProvider[]}
|
|
728
|
+
*/
|
|
729
|
+
listIdentityProviders(input) {
|
|
730
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
731
|
+
const status = input.status ? nonEmptyString("status", input.status) : null;
|
|
732
|
+
const sql = status
|
|
733
|
+
? `
|
|
734
|
+
SELECT org_id AS orgId, provider_id AS providerId, type, issuer, sso_url AS ssoUrl, certificate_ref AS certificateRef, status, metadata_json AS metadataJson, created_at_ms AS createdAtMs, updated_at_ms AS updatedAtMs
|
|
735
|
+
FROM _smithers_cp_identity_providers
|
|
736
|
+
WHERE org_id = ? AND status = ?
|
|
737
|
+
ORDER BY provider_id
|
|
738
|
+
`
|
|
739
|
+
: `
|
|
740
|
+
SELECT org_id AS orgId, provider_id AS providerId, type, issuer, sso_url AS ssoUrl, certificate_ref AS certificateRef, status, metadata_json AS metadataJson, created_at_ms AS createdAtMs, updated_at_ms AS updatedAtMs
|
|
741
|
+
FROM _smithers_cp_identity_providers
|
|
742
|
+
WHERE org_id = ?
|
|
743
|
+
ORDER BY provider_id
|
|
744
|
+
`;
|
|
745
|
+
const args = status ? [orgId, status] : [orgId];
|
|
746
|
+
return this.sqlite.query(sql).all(...args).map(identityProviderRow);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* @param {{ orgId: string; projectId?: string | null; runId?: string | null; metric: string; quantity: number; unit?: string; observedAtMs?: number; metadata?: Record<string, unknown> }} input
|
|
751
|
+
* @returns {ControlPlaneUsageEvent}
|
|
752
|
+
*/
|
|
753
|
+
recordUsage(input) {
|
|
754
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
755
|
+
const projectId = input.projectId ? requiredId("projectId", input.projectId) : null;
|
|
756
|
+
const observedAtMs = timestamp(input.observedAtMs);
|
|
757
|
+
const metadata = jsonObject(input.metadata);
|
|
758
|
+
this.sqlite.query(`
|
|
759
|
+
INSERT INTO _smithers_cp_usage_events (org_id, project_id, run_id, metric, quantity, unit, observed_at_ms, metadata_json)
|
|
760
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
761
|
+
`).run(
|
|
762
|
+
orgId,
|
|
763
|
+
projectId,
|
|
764
|
+
input.runId ? requiredId("runId", input.runId) : null,
|
|
765
|
+
nonEmptyString("metric", input.metric),
|
|
766
|
+
quantity(input.quantity),
|
|
767
|
+
nonEmptyString("unit", input.unit ?? "count"),
|
|
768
|
+
observedAtMs,
|
|
769
|
+
JSON.stringify(metadata),
|
|
770
|
+
);
|
|
771
|
+
const id = this.sqlite.query("SELECT last_insert_rowid() AS id").get().id;
|
|
772
|
+
const row = this.sqlite.query(`
|
|
773
|
+
SELECT id, org_id AS orgId, project_id AS projectId, run_id AS runId, metric, quantity, unit, observed_at_ms AS observedAtMs, metadata_json AS metadataJson
|
|
774
|
+
FROM _smithers_cp_usage_events
|
|
775
|
+
WHERE id = ?
|
|
776
|
+
LIMIT 1
|
|
777
|
+
`).get(id);
|
|
778
|
+
return usageRow(row);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* @param {{ orgId: string; sinceMs?: number; untilMs?: number }} input
|
|
783
|
+
* @returns {ControlPlaneUsageSummary[]}
|
|
784
|
+
*/
|
|
785
|
+
summarizeUsage(input) {
|
|
786
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
787
|
+
const sinceMs = input.sinceMs === undefined ? 0 : timestamp(input.sinceMs);
|
|
788
|
+
const untilMs = input.untilMs === undefined ? Number.MAX_SAFE_INTEGER : timestamp(input.untilMs);
|
|
789
|
+
return this.sqlite.query(`
|
|
790
|
+
SELECT org_id AS orgId, metric, unit, SUM(quantity) AS quantity
|
|
791
|
+
FROM _smithers_cp_usage_events
|
|
792
|
+
WHERE org_id = ? AND observed_at_ms >= ? AND observed_at_ms <= ?
|
|
793
|
+
GROUP BY org_id, metric, unit
|
|
794
|
+
ORDER BY metric, unit
|
|
795
|
+
`).all(orgId, sinceMs, untilMs).map((row) => ({
|
|
796
|
+
orgId: String(row.orgId),
|
|
797
|
+
metric: String(row.metric),
|
|
798
|
+
unit: String(row.unit),
|
|
799
|
+
quantity: Number(row.quantity),
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* @param {{ orgId: string; projectId?: string | null; metric: string; unit?: string; period?: string; limitQuantity: number; updatedAtMs?: number }} input
|
|
805
|
+
* @returns {ControlPlaneUsageLimit}
|
|
806
|
+
*/
|
|
807
|
+
setUsageLimit(input) {
|
|
808
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
809
|
+
const projectId = input.projectId ? requiredId("projectId", input.projectId) : null;
|
|
810
|
+
if (projectId) {
|
|
811
|
+
assertProjectExists(this.sqlite, orgId, projectId);
|
|
812
|
+
}
|
|
813
|
+
const metric = nonEmptyString("metric", input.metric);
|
|
814
|
+
const unit = nonEmptyString("unit", input.unit ?? "count");
|
|
815
|
+
const period = nonEmptyString("period", input.period ?? "monthly");
|
|
816
|
+
const limitValue = quantity(input.limitQuantity);
|
|
817
|
+
const updatedAtMs = timestamp(input.updatedAtMs);
|
|
818
|
+
this.sqlite.query(`
|
|
819
|
+
INSERT INTO _smithers_cp_usage_limits (org_id, project_key, project_id, metric, unit, period, limit_quantity, updated_at_ms)
|
|
820
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
821
|
+
ON CONFLICT(org_id, project_key, metric, unit, period) DO UPDATE SET
|
|
822
|
+
project_id = excluded.project_id,
|
|
823
|
+
limit_quantity = excluded.limit_quantity,
|
|
824
|
+
updated_at_ms = excluded.updated_at_ms
|
|
825
|
+
`).run(orgId, projectKey(projectId), projectId, metric, unit, period, limitValue, updatedAtMs);
|
|
826
|
+
this.recordAuditEvent({
|
|
827
|
+
orgId,
|
|
828
|
+
projectId,
|
|
829
|
+
action: "usage_limit.upsert",
|
|
830
|
+
targetType: "usage_limit",
|
|
831
|
+
targetId: projectId ?? orgId,
|
|
832
|
+
occurredAtMs: updatedAtMs,
|
|
833
|
+
metadata: { metric, unit, period, limitQuantity: limitValue },
|
|
834
|
+
});
|
|
835
|
+
const row = this.sqlite.query(`
|
|
836
|
+
SELECT org_id AS orgId, project_id AS projectId, metric, unit, period, limit_quantity AS limitQuantity, updated_at_ms AS updatedAtMs
|
|
837
|
+
FROM _smithers_cp_usage_limits
|
|
838
|
+
WHERE org_id = ? AND project_key = ? AND metric = ? AND unit = ? AND period = ?
|
|
839
|
+
LIMIT 1
|
|
840
|
+
`).get(orgId, projectKey(projectId), metric, unit, period);
|
|
841
|
+
return usageLimitRow(row);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* @param {{ orgId: string; projectId?: string | null; metric: string; unit?: string; period?: string; sinceMs?: number; untilMs?: number }} input
|
|
846
|
+
* @returns {ControlPlaneUsageLimitCheck | null}
|
|
847
|
+
*/
|
|
848
|
+
checkUsageLimit(input) {
|
|
849
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
850
|
+
const projectId = input.projectId ? requiredId("projectId", input.projectId) : null;
|
|
851
|
+
const metric = nonEmptyString("metric", input.metric);
|
|
852
|
+
const unit = nonEmptyString("unit", input.unit ?? "count");
|
|
853
|
+
const period = nonEmptyString("period", input.period ?? "monthly");
|
|
854
|
+
const limitRowRaw = this.sqlite.query(`
|
|
855
|
+
SELECT org_id AS orgId, project_id AS projectId, metric, unit, period, limit_quantity AS limitQuantity, updated_at_ms AS updatedAtMs
|
|
856
|
+
FROM _smithers_cp_usage_limits
|
|
857
|
+
WHERE org_id = ? AND project_key = ? AND metric = ? AND unit = ? AND period = ?
|
|
858
|
+
LIMIT 1
|
|
859
|
+
`).get(orgId, projectKey(projectId), metric, unit, period);
|
|
860
|
+
if (!limitRowRaw) {
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
const sinceMs = input.sinceMs === undefined ? 0 : timestamp(input.sinceMs);
|
|
864
|
+
const untilMs = input.untilMs === undefined ? Number.MAX_SAFE_INTEGER : timestamp(input.untilMs);
|
|
865
|
+
const usageSql = projectId
|
|
866
|
+
? `
|
|
867
|
+
SELECT COALESCE(SUM(quantity), 0) AS usedQuantity
|
|
868
|
+
FROM _smithers_cp_usage_events
|
|
869
|
+
WHERE org_id = ? AND project_id = ? AND metric = ? AND unit = ? AND observed_at_ms >= ? AND observed_at_ms <= ?
|
|
870
|
+
`
|
|
871
|
+
: `
|
|
872
|
+
SELECT COALESCE(SUM(quantity), 0) AS usedQuantity
|
|
873
|
+
FROM _smithers_cp_usage_events
|
|
874
|
+
WHERE org_id = ? AND metric = ? AND unit = ? AND observed_at_ms >= ? AND observed_at_ms <= ?
|
|
875
|
+
`;
|
|
876
|
+
const usageArgs = projectId
|
|
877
|
+
? [orgId, projectId, metric, unit, sinceMs, untilMs]
|
|
878
|
+
: [orgId, metric, unit, sinceMs, untilMs];
|
|
879
|
+
const usageRowRaw = this.sqlite.query(usageSql).get(...usageArgs);
|
|
880
|
+
const limit = usageLimitRow(limitRowRaw);
|
|
881
|
+
const usedQuantity = Number(usageRowRaw?.usedQuantity ?? 0);
|
|
882
|
+
const remainingQuantity = Math.max(0, limit.limitQuantity - usedQuantity);
|
|
883
|
+
return {
|
|
884
|
+
...limit,
|
|
885
|
+
usedQuantity,
|
|
886
|
+
remainingQuantity,
|
|
887
|
+
exceeded: usedQuantity > limit.limitQuantity,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* @param {{ orgId: string; projectId?: string | null; name: string; provider: string; ref: string; createdBy?: string | null; createdAtMs?: number; rotatedAtMs?: number | null }} input
|
|
893
|
+
* @returns {ControlPlaneSecretRef}
|
|
894
|
+
*/
|
|
895
|
+
putSecretRef(input) {
|
|
896
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
897
|
+
const projectId = input.projectId ? requiredId("projectId", input.projectId) : null;
|
|
898
|
+
if (projectId) {
|
|
899
|
+
assertProjectExists(this.sqlite, orgId, projectId);
|
|
900
|
+
}
|
|
901
|
+
const secretProjectKey = projectKey(projectId);
|
|
902
|
+
const name = nonEmptyString("name", input.name);
|
|
903
|
+
const createdAtMs = timestamp(input.createdAtMs);
|
|
904
|
+
const rotatedAtMs = input.rotatedAtMs === undefined || input.rotatedAtMs === null
|
|
905
|
+
? null
|
|
906
|
+
: timestamp(input.rotatedAtMs);
|
|
907
|
+
const provider = nonEmptyString("provider", input.provider);
|
|
908
|
+
const ref = nonEmptyString("ref", input.ref);
|
|
909
|
+
const createdBy = input.createdBy ? requiredId("createdBy", input.createdBy) : null;
|
|
910
|
+
this.sqlite.query(`
|
|
911
|
+
DELETE FROM _smithers_cp_secret_refs
|
|
912
|
+
WHERE org_id = ? AND project_key = ? AND name = ?
|
|
913
|
+
`).run(orgId, secretProjectKey, name);
|
|
914
|
+
this.sqlite.query(`
|
|
915
|
+
INSERT INTO _smithers_cp_secret_refs (org_id, project_key, project_id, name, provider, ref, created_by, created_at_ms, rotated_at_ms)
|
|
916
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
917
|
+
`).run(
|
|
918
|
+
orgId,
|
|
919
|
+
secretProjectKey,
|
|
920
|
+
projectId,
|
|
921
|
+
name,
|
|
922
|
+
provider,
|
|
923
|
+
ref,
|
|
924
|
+
createdBy,
|
|
925
|
+
createdAtMs,
|
|
926
|
+
rotatedAtMs,
|
|
927
|
+
);
|
|
928
|
+
this.recordAuditEvent({
|
|
929
|
+
orgId,
|
|
930
|
+
projectId,
|
|
931
|
+
actorId: createdBy,
|
|
932
|
+
action: "secret_ref.upsert",
|
|
933
|
+
targetType: "secret_ref",
|
|
934
|
+
targetId: name,
|
|
935
|
+
occurredAtMs: rotatedAtMs ?? createdAtMs,
|
|
936
|
+
metadata: { provider },
|
|
937
|
+
});
|
|
938
|
+
const row = this.sqlite.query(`
|
|
939
|
+
SELECT org_id AS orgId, project_id AS projectId, name, provider, ref, created_by AS createdBy, created_at_ms AS createdAtMs, rotated_at_ms AS rotatedAtMs
|
|
940
|
+
FROM _smithers_cp_secret_refs
|
|
941
|
+
WHERE org_id = ? AND project_key = ? AND name = ?
|
|
942
|
+
LIMIT 1
|
|
943
|
+
`).get(orgId, secretProjectKey, name);
|
|
944
|
+
return secretRefRow(row);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* @param {{ orgId: string; projectId?: string | null }} input
|
|
949
|
+
* @returns {ControlPlaneSecretRef[]}
|
|
950
|
+
*/
|
|
951
|
+
listSecretRefs(input) {
|
|
952
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
953
|
+
const projectId = input.projectId === undefined ? undefined : input.projectId;
|
|
954
|
+
const sql = projectId === undefined
|
|
955
|
+
? `
|
|
956
|
+
SELECT org_id AS orgId, project_id AS projectId, name, provider, ref, created_by AS createdBy, created_at_ms AS createdAtMs, rotated_at_ms AS rotatedAtMs
|
|
957
|
+
FROM _smithers_cp_secret_refs
|
|
958
|
+
WHERE org_id = ?
|
|
959
|
+
ORDER BY name
|
|
960
|
+
`
|
|
961
|
+
: `
|
|
962
|
+
SELECT org_id AS orgId, project_id AS projectId, name, provider, ref, created_by AS createdBy, created_at_ms AS createdAtMs, rotated_at_ms AS rotatedAtMs
|
|
963
|
+
FROM _smithers_cp_secret_refs
|
|
964
|
+
WHERE org_id = ? AND project_key = ?
|
|
965
|
+
ORDER BY name
|
|
966
|
+
`;
|
|
967
|
+
const args = projectId === undefined ? [orgId] : [orgId, projectKey(projectId ? requiredId("projectId", projectId) : null)];
|
|
968
|
+
return this.sqlite.query(sql).all(...args).map(secretRefRow);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* @param {{ orgId: string; projectId?: string | null; actorId?: string | null; action: string; targetType: string; targetId?: string | null; occurredAtMs?: number; metadata?: Record<string, unknown> }} input
|
|
973
|
+
* @returns {ControlPlaneAuditEvent}
|
|
974
|
+
*/
|
|
975
|
+
recordAuditEvent(input) {
|
|
976
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
977
|
+
const projectId = input.projectId ? requiredId("projectId", input.projectId) : null;
|
|
978
|
+
const occurredAtMs = timestamp(input.occurredAtMs);
|
|
979
|
+
const metadata = jsonObject(input.metadata);
|
|
980
|
+
this.sqlite.query(`
|
|
981
|
+
INSERT INTO _smithers_cp_audit_events (org_id, project_id, actor_id, action, target_type, target_id, occurred_at_ms, metadata_json)
|
|
982
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
983
|
+
`).run(
|
|
984
|
+
orgId,
|
|
985
|
+
projectId,
|
|
986
|
+
input.actorId ? requiredId("actorId", input.actorId) : null,
|
|
987
|
+
nonEmptyString("action", input.action),
|
|
988
|
+
nonEmptyString("targetType", input.targetType),
|
|
989
|
+
input.targetId ? requiredId("targetId", input.targetId) : null,
|
|
990
|
+
occurredAtMs,
|
|
991
|
+
JSON.stringify(metadata),
|
|
992
|
+
);
|
|
993
|
+
const id = this.sqlite.query("SELECT last_insert_rowid() AS id").get().id;
|
|
994
|
+
const row = this.sqlite.query(`
|
|
995
|
+
SELECT id, org_id AS orgId, project_id AS projectId, actor_id AS actorId, action, target_type AS targetType, target_id AS targetId, occurred_at_ms AS occurredAtMs, metadata_json AS metadataJson
|
|
996
|
+
FROM _smithers_cp_audit_events
|
|
997
|
+
WHERE id = ?
|
|
998
|
+
LIMIT 1
|
|
999
|
+
`).get(id);
|
|
1000
|
+
return auditRow(row);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* @param {{ orgId: string; sinceMs?: number; untilMs?: number; exportedAtMs?: number }} input
|
|
1005
|
+
* @returns {ControlPlaneExport}
|
|
1006
|
+
*/
|
|
1007
|
+
exportOrgAudit(input) {
|
|
1008
|
+
const orgId = requiredId("orgId", input.orgId);
|
|
1009
|
+
const org = this.getOrg(orgId);
|
|
1010
|
+
if (!org) {
|
|
1011
|
+
throw new SmithersError("INVALID_INPUT", `Control-plane org not found: ${orgId}`, { orgId });
|
|
1012
|
+
}
|
|
1013
|
+
const sinceMs = input.sinceMs === undefined ? 0 : timestamp(input.sinceMs);
|
|
1014
|
+
const untilMs = input.untilMs === undefined ? Number.MAX_SAFE_INTEGER : timestamp(input.untilMs);
|
|
1015
|
+
const projects = this.sqlite.query(`
|
|
1016
|
+
SELECT org_id AS orgId, project_id AS projectId, slug, name, metadata_json AS metadataJson, created_at_ms AS createdAtMs
|
|
1017
|
+
FROM _smithers_cp_projects
|
|
1018
|
+
WHERE org_id = ?
|
|
1019
|
+
ORDER BY slug
|
|
1020
|
+
`).all(orgId).map(projectRow);
|
|
1021
|
+
const teams = this.sqlite.query(`
|
|
1022
|
+
SELECT org_id AS orgId, team_id AS teamId, slug, name, created_at_ms AS createdAtMs
|
|
1023
|
+
FROM _smithers_cp_teams
|
|
1024
|
+
WHERE org_id = ?
|
|
1025
|
+
ORDER BY slug
|
|
1026
|
+
`).all(orgId).map(teamRow);
|
|
1027
|
+
const billingRaw = this.sqlite.query(`
|
|
1028
|
+
SELECT org_id AS orgId, plan, billing_customer_id AS billingCustomerId, status, updated_at_ms AS updatedAtMs
|
|
1029
|
+
FROM _smithers_cp_billing_accounts
|
|
1030
|
+
WHERE org_id = ?
|
|
1031
|
+
LIMIT 1
|
|
1032
|
+
`).get(orgId);
|
|
1033
|
+
const identityProviders = this.sqlite.query(`
|
|
1034
|
+
SELECT org_id AS orgId, provider_id AS providerId, type, issuer, sso_url AS ssoUrl, certificate_ref AS certificateRef, status, metadata_json AS metadataJson, created_at_ms AS createdAtMs, updated_at_ms AS updatedAtMs
|
|
1035
|
+
FROM _smithers_cp_identity_providers
|
|
1036
|
+
WHERE org_id = ?
|
|
1037
|
+
ORDER BY provider_id
|
|
1038
|
+
`).all(orgId).map(identityProviderRow);
|
|
1039
|
+
const usageLimits = this.sqlite.query(`
|
|
1040
|
+
SELECT org_id AS orgId, project_id AS projectId, metric, unit, period, limit_quantity AS limitQuantity, updated_at_ms AS updatedAtMs
|
|
1041
|
+
FROM _smithers_cp_usage_limits
|
|
1042
|
+
WHERE org_id = ?
|
|
1043
|
+
ORDER BY project_key, metric, unit, period
|
|
1044
|
+
`).all(orgId).map(usageLimitRow);
|
|
1045
|
+
const auditEvents = this.sqlite.query(`
|
|
1046
|
+
SELECT id, org_id AS orgId, project_id AS projectId, actor_id AS actorId, action, target_type AS targetType, target_id AS targetId, occurred_at_ms AS occurredAtMs, metadata_json AS metadataJson
|
|
1047
|
+
FROM _smithers_cp_audit_events
|
|
1048
|
+
WHERE org_id = ? AND occurred_at_ms >= ? AND occurred_at_ms <= ?
|
|
1049
|
+
ORDER BY occurred_at_ms, id
|
|
1050
|
+
`).all(orgId, sinceMs, untilMs).map(auditRow);
|
|
1051
|
+
return {
|
|
1052
|
+
exportedAtMs: timestamp(input.exportedAtMs),
|
|
1053
|
+
org,
|
|
1054
|
+
projects,
|
|
1055
|
+
teams,
|
|
1056
|
+
billing: billingRaw ? billingRow(billingRaw) : null,
|
|
1057
|
+
identityProviders,
|
|
1058
|
+
usage: this.summarizeUsage({ orgId, sinceMs, untilMs }),
|
|
1059
|
+
usageLimits,
|
|
1060
|
+
secretRefs: this.listSecretRefs({ orgId }),
|
|
1061
|
+
auditEvents,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|