@smithers-orchestrator/control-plane 0.24.2 → 0.25.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/package.json +2 -2
- package/src/index.d.ts +6 -4
- package/src/index.js +98 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/control-plane",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Durable organization, project, billing, usage, secret-reference, and audit primitives for hosted Smithers control planes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"src/"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@smithers-orchestrator/errors": "0.
|
|
23
|
+
"@smithers-orchestrator/errors": "0.25.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/index.d.ts
CHANGED
|
@@ -66,12 +66,14 @@ type ControlPlaneUsageEvent = {
|
|
|
66
66
|
metadata: Record<string, unknown>;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
+
type ControlPlaneUsageLimitPeriod = 'daily' | 'weekly' | 'monthly';
|
|
70
|
+
|
|
69
71
|
type ControlPlaneUsageLimit = {
|
|
70
72
|
orgId: string;
|
|
71
73
|
projectId: string | null;
|
|
72
74
|
metric: string;
|
|
73
75
|
unit: string;
|
|
74
|
-
period:
|
|
76
|
+
period: ControlPlaneUsageLimitPeriod;
|
|
75
77
|
limitQuantity: number;
|
|
76
78
|
updatedAtMs: number;
|
|
77
79
|
};
|
|
@@ -141,12 +143,12 @@ declare class ControlPlaneStore {
|
|
|
141
143
|
listIdentityProviders(input: { orgId: string; status?: string }): ControlPlaneIdentityProvider[];
|
|
142
144
|
recordUsage(input: { orgId: string; projectId?: string | null; runId?: string | null; metric: string; quantity: number; unit?: string; observedAtMs?: number; metadata?: Record<string, unknown> }): ControlPlaneUsageEvent;
|
|
143
145
|
summarizeUsage(input: { orgId: string; sinceMs?: number; untilMs?: number }): ControlPlaneUsageSummary[];
|
|
144
|
-
setUsageLimit(input: { orgId: string; projectId?: string | null; metric: string; unit?: string; period?:
|
|
145
|
-
checkUsageLimit(input: { orgId: string; projectId?: string | null; metric: string; unit?: string; period?:
|
|
146
|
+
setUsageLimit(input: { orgId: string; projectId?: string | null; metric: string; unit?: string; period?: ControlPlaneUsageLimitPeriod; limitQuantity: number; updatedAtMs?: number }): ControlPlaneUsageLimit;
|
|
147
|
+
checkUsageLimit(input: { orgId: string; projectId?: string | null; metric: string; unit?: string; period?: ControlPlaneUsageLimitPeriod; sinceMs?: number; untilMs?: number }): ControlPlaneUsageLimitCheck | null;
|
|
146
148
|
putSecretRef(input: { orgId: string; projectId?: string | null; name: string; provider: string; ref: string; createdBy?: string | null; createdAtMs?: number; rotatedAtMs?: number | null }): ControlPlaneSecretRef;
|
|
147
149
|
listSecretRefs(input: { orgId: string; projectId?: string | null }): ControlPlaneSecretRef[];
|
|
148
150
|
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
151
|
exportOrgAudit(input: { orgId: string; sinceMs?: number; untilMs?: number; exportedAtMs?: number }): ControlPlaneExport;
|
|
150
152
|
}
|
|
151
153
|
|
|
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 };
|
|
154
|
+
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 ControlPlaneUsageLimitPeriod, type ControlPlaneUsageSummary, ensureControlPlaneTables };
|
package/src/index.js
CHANGED
|
@@ -19,6 +19,11 @@ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
|
19
19
|
|
|
20
20
|
const SLUG_RE = /^(?:[a-z0-9]|[a-z0-9][a-z0-9-]{0,62}[a-z0-9])$/;
|
|
21
21
|
const ID_RE = /^[A-Za-z0-9:_-]{1,128}$/;
|
|
22
|
+
const USAGE_LIMIT_PERIODS = new Map([
|
|
23
|
+
["daily", 24 * 60 * 60 * 1000],
|
|
24
|
+
["weekly", 7 * 24 * 60 * 60 * 1000],
|
|
25
|
+
["monthly", 30 * 24 * 60 * 60 * 1000],
|
|
26
|
+
]);
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* @param {ControlPlaneSqlite} sqlite
|
|
@@ -285,7 +290,16 @@ function parseJsonObject(value) {
|
|
|
285
290
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
286
291
|
return {};
|
|
287
292
|
}
|
|
288
|
-
|
|
293
|
+
let parsed;
|
|
294
|
+
try {
|
|
295
|
+
parsed = JSON.parse(value);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
console.warn("control-plane: ignoring malformed metadata_json.", {
|
|
299
|
+
error: error instanceof Error ? error.message : String(error),
|
|
300
|
+
});
|
|
301
|
+
return {};
|
|
302
|
+
}
|
|
289
303
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
290
304
|
}
|
|
291
305
|
|
|
@@ -311,6 +325,17 @@ function quantity(value) {
|
|
|
311
325
|
return n;
|
|
312
326
|
}
|
|
313
327
|
|
|
328
|
+
/**
|
|
329
|
+
* @param {unknown} value
|
|
330
|
+
*/
|
|
331
|
+
function usageLimitPeriod(value) {
|
|
332
|
+
const period = nonEmptyString("period", value);
|
|
333
|
+
if (!USAGE_LIMIT_PERIODS.has(period)) {
|
|
334
|
+
throw new SmithersError("INVALID_INPUT", "period must be one of: daily, weekly, monthly.", { period });
|
|
335
|
+
}
|
|
336
|
+
return period;
|
|
337
|
+
}
|
|
338
|
+
|
|
314
339
|
/**
|
|
315
340
|
* @param {string | null} projectId
|
|
316
341
|
*/
|
|
@@ -318,6 +343,33 @@ function projectKey(projectId) {
|
|
|
318
343
|
return projectId ?? "__org__";
|
|
319
344
|
}
|
|
320
345
|
|
|
346
|
+
/**
|
|
347
|
+
* @param {unknown} error
|
|
348
|
+
* @param {string} constraint
|
|
349
|
+
*/
|
|
350
|
+
function isUniqueConstraintError(error, constraint) {
|
|
351
|
+
return error instanceof Error && error.message.includes(`UNIQUE constraint failed: ${constraint}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @param {unknown} error
|
|
356
|
+
* @param {{ entity: "org" | "team" | "project"; slug: string; orgId?: string }} input
|
|
357
|
+
*/
|
|
358
|
+
function throwDuplicateSlugError(error, input) {
|
|
359
|
+
const details = {
|
|
360
|
+
kind: `control-plane.${input.entity}`,
|
|
361
|
+
id: input.slug,
|
|
362
|
+
slug: input.slug,
|
|
363
|
+
...(input.orgId ? { orgId: input.orgId } : {}),
|
|
364
|
+
};
|
|
365
|
+
throw new SmithersError(
|
|
366
|
+
"DUPLICATE_ID",
|
|
367
|
+
`Duplicate control-plane ${input.entity} slug: ${input.slug}`,
|
|
368
|
+
details,
|
|
369
|
+
{ cause: error },
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
321
373
|
/**
|
|
322
374
|
* @param {Record<string, unknown>} row
|
|
323
375
|
* @returns {ControlPlaneOrg}
|
|
@@ -421,7 +473,7 @@ function usageLimitRow(row) {
|
|
|
421
473
|
projectId: row.projectId === null ? null : String(row.projectId),
|
|
422
474
|
metric: String(row.metric),
|
|
423
475
|
unit: String(row.unit),
|
|
424
|
-
period:
|
|
476
|
+
period: usageLimitPeriod(row.period),
|
|
425
477
|
limitQuantity: Number(row.limitQuantity),
|
|
426
478
|
updatedAtMs: Number(row.updatedAtMs),
|
|
427
479
|
};
|
|
@@ -497,11 +549,20 @@ export class ControlPlaneStore {
|
|
|
497
549
|
*/
|
|
498
550
|
createOrg(input) {
|
|
499
551
|
const orgId = optionalId("orgId", input.orgId);
|
|
552
|
+
const orgSlug = slug("slug", input.slug);
|
|
500
553
|
const createdAtMs = timestamp(input.createdAtMs);
|
|
501
|
-
|
|
554
|
+
try {
|
|
555
|
+
this.sqlite.query(`
|
|
502
556
|
INSERT INTO _smithers_cp_orgs (org_id, slug, name, created_at_ms)
|
|
503
557
|
VALUES (?, ?, ?, ?)
|
|
504
|
-
`).run(orgId,
|
|
558
|
+
`).run(orgId, orgSlug, nonEmptyString("name", input.name), createdAtMs);
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
if (isUniqueConstraintError(error, "_smithers_cp_orgs.slug")) {
|
|
562
|
+
throwDuplicateSlugError(error, { entity: "org", slug: orgSlug });
|
|
563
|
+
}
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
505
566
|
this.recordAuditEvent({
|
|
506
567
|
orgId,
|
|
507
568
|
actorId: "system",
|
|
@@ -534,11 +595,20 @@ LIMIT 1
|
|
|
534
595
|
createTeam(input) {
|
|
535
596
|
const orgId = requiredId("orgId", input.orgId);
|
|
536
597
|
const teamId = optionalId("teamId", input.teamId);
|
|
598
|
+
const teamSlug = slug("slug", input.slug);
|
|
537
599
|
const createdAtMs = timestamp(input.createdAtMs);
|
|
538
|
-
|
|
600
|
+
try {
|
|
601
|
+
this.sqlite.query(`
|
|
539
602
|
INSERT INTO _smithers_cp_teams (org_id, team_id, slug, name, created_at_ms)
|
|
540
603
|
VALUES (?, ?, ?, ?, ?)
|
|
541
|
-
`).run(orgId, teamId,
|
|
604
|
+
`).run(orgId, teamId, teamSlug, nonEmptyString("name", input.name), createdAtMs);
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
if (isUniqueConstraintError(error, "_smithers_cp_teams.org_id, _smithers_cp_teams.slug")) {
|
|
608
|
+
throwDuplicateSlugError(error, { entity: "team", slug: teamSlug, orgId });
|
|
609
|
+
}
|
|
610
|
+
throw error;
|
|
611
|
+
}
|
|
542
612
|
this.recordAuditEvent({
|
|
543
613
|
orgId,
|
|
544
614
|
action: "team.create",
|
|
@@ -587,12 +657,21 @@ ON CONFLICT(org_id, team_id, user_id) DO UPDATE SET role = excluded.role
|
|
|
587
657
|
createProject(input) {
|
|
588
658
|
const orgId = requiredId("orgId", input.orgId);
|
|
589
659
|
const projectId = optionalId("projectId", input.projectId);
|
|
660
|
+
const projectSlug = slug("slug", input.slug);
|
|
590
661
|
const metadata = jsonObject(input.metadata);
|
|
591
662
|
const createdAtMs = timestamp(input.createdAtMs);
|
|
592
|
-
|
|
663
|
+
try {
|
|
664
|
+
this.sqlite.query(`
|
|
593
665
|
INSERT INTO _smithers_cp_projects (org_id, project_id, slug, name, metadata_json, created_at_ms)
|
|
594
666
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
595
|
-
`).run(orgId, projectId,
|
|
667
|
+
`).run(orgId, projectId, projectSlug, nonEmptyString("name", input.name), JSON.stringify(metadata), createdAtMs);
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
if (isUniqueConstraintError(error, "_smithers_cp_projects.org_id, _smithers_cp_projects.slug")) {
|
|
671
|
+
throwDuplicateSlugError(error, { entity: "project", slug: projectSlug, orgId });
|
|
672
|
+
}
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
596
675
|
this.recordAuditEvent({
|
|
597
676
|
orgId,
|
|
598
677
|
projectId,
|
|
@@ -753,6 +832,9 @@ ORDER BY provider_id
|
|
|
753
832
|
recordUsage(input) {
|
|
754
833
|
const orgId = requiredId("orgId", input.orgId);
|
|
755
834
|
const projectId = input.projectId ? requiredId("projectId", input.projectId) : null;
|
|
835
|
+
if (projectId) {
|
|
836
|
+
assertProjectExists(this.sqlite, orgId, projectId);
|
|
837
|
+
}
|
|
756
838
|
const observedAtMs = timestamp(input.observedAtMs);
|
|
757
839
|
const metadata = jsonObject(input.metadata);
|
|
758
840
|
this.sqlite.query(`
|
|
@@ -816,7 +898,7 @@ ORDER BY metric, unit
|
|
|
816
898
|
}
|
|
817
899
|
const metric = nonEmptyString("metric", input.metric);
|
|
818
900
|
const unit = nonEmptyString("unit", input.unit ?? "count");
|
|
819
|
-
const period =
|
|
901
|
+
const period = usageLimitPeriod(input.period ?? "monthly");
|
|
820
902
|
const limitValue = quantity(input.limitQuantity);
|
|
821
903
|
const updatedAtMs = timestamp(input.updatedAtMs);
|
|
822
904
|
this.sqlite.query(`
|
|
@@ -854,7 +936,7 @@ LIMIT 1
|
|
|
854
936
|
const projectId = input.projectId ? requiredId("projectId", input.projectId) : null;
|
|
855
937
|
const metric = nonEmptyString("metric", input.metric);
|
|
856
938
|
const unit = nonEmptyString("unit", input.unit ?? "count");
|
|
857
|
-
const period =
|
|
939
|
+
const period = usageLimitPeriod(input.period ?? "monthly");
|
|
858
940
|
const limitRowRaw = this.sqlite.query(`
|
|
859
941
|
SELECT org_id AS orgId, project_id AS projectId, metric, unit, period, limit_quantity AS limitQuantity, updated_at_ms AS updatedAtMs
|
|
860
942
|
FROM _smithers_cp_usage_limits
|
|
@@ -864,8 +946,9 @@ LIMIT 1
|
|
|
864
946
|
if (!limitRowRaw) {
|
|
865
947
|
return null;
|
|
866
948
|
}
|
|
867
|
-
const
|
|
868
|
-
const
|
|
949
|
+
const untilMs = input.untilMs === undefined ? timestamp(undefined) : timestamp(input.untilMs);
|
|
950
|
+
const periodMs = USAGE_LIMIT_PERIODS.get(period) ?? 0;
|
|
951
|
+
const sinceMs = input.sinceMs === undefined ? Math.max(0, untilMs - periodMs) : timestamp(input.sinceMs);
|
|
869
952
|
const usageSql = projectId
|
|
870
953
|
? `
|
|
871
954
|
SELECT COALESCE(SUM(quantity), 0) AS usedQuantity
|
|
@@ -979,6 +1062,9 @@ ORDER BY name
|
|
|
979
1062
|
recordAuditEvent(input) {
|
|
980
1063
|
const orgId = requiredId("orgId", input.orgId);
|
|
981
1064
|
const projectId = input.projectId ? requiredId("projectId", input.projectId) : null;
|
|
1065
|
+
if (projectId) {
|
|
1066
|
+
assertProjectExists(this.sqlite, orgId, projectId);
|
|
1067
|
+
}
|
|
982
1068
|
const occurredAtMs = timestamp(input.occurredAtMs);
|
|
983
1069
|
const metadata = jsonObject(input.metadata);
|
|
984
1070
|
this.sqlite.query(`
|