@superblocksteam/shared 0.9590.9 → 0.9591.1
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/dist/database-lifecycle/index.d.ts +33 -74
- package/dist/database-lifecycle/index.d.ts.map +1 -1
- package/dist/database-lifecycle/index.js +33 -31
- package/dist/database-lifecycle/index.js.map +1 -1
- package/dist/database-lifecycle/index.test.js +6 -6
- package/dist/database-lifecycle/index.test.js.map +1 -1
- package/dist/socket/protocol.d.ts +14 -1
- package/dist/socket/protocol.d.ts.map +1 -1
- package/dist/socket/protocol.js.map +1 -1
- package/dist/types/ai/index.d.ts +1 -0
- package/dist/types/ai/index.d.ts.map +1 -1
- package/dist/types/ai/index.js +1 -0
- package/dist/types/ai/index.js.map +1 -1
- package/dist/types/ai/quota-paywall.d.ts +8 -0
- package/dist/types/ai/quota-paywall.d.ts.map +1 -1
- package/dist/types/ai/quota-paywall.js +19 -1
- package/dist/types/ai/quota-paywall.js.map +1 -1
- package/dist/types/ai/safety-classification.d.ts +16 -0
- package/dist/types/ai/safety-classification.d.ts.map +1 -0
- package/dist/types/ai/safety-classification.js +22 -0
- package/dist/types/ai/safety-classification.js.map +1 -0
- package/dist/types/audit/ocsf.d.ts +32 -0
- package/dist/types/audit/ocsf.d.ts.map +1 -1
- package/dist/types/audit/ocsf.js +2 -0
- package/dist/types/audit/ocsf.js.map +1 -1
- package/dist/types/billing/billing.d.ts +1 -0
- package/dist/types/billing/billing.d.ts.map +1 -1
- package/dist/types/billing/billing.js +10 -0
- package/dist/types/billing/billing.js.map +1 -1
- package/dist/types/billing/index.d.ts +1 -0
- package/dist/types/billing/index.d.ts.map +1 -1
- package/dist/types/billing/index.js +1 -0
- package/dist/types/billing/index.js.map +1 -1
- package/dist/types/billing/spendAlert.d.ts +62 -0
- package/dist/types/billing/spendAlert.d.ts.map +1 -0
- package/dist/types/billing/spendAlert.js +11 -0
- package/dist/types/billing/spendAlert.js.map +1 -0
- package/dist/types/policyGate/index.d.ts +24 -0
- package/dist/types/policyGate/index.d.ts.map +1 -1
- package/dist/types/policyGate/index.js +9 -0
- package/dist/types/policyGate/index.js.map +1 -1
- package/dist/types/rbac/index.d.ts +4 -0
- package/dist/types/rbac/index.d.ts.map +1 -1
- package/dist/types/rbac/index.js +4 -0
- package/dist/types/rbac/index.js.map +1 -1
- package/dist/types/reviewPolicy/index.d.ts +9 -3
- package/dist/types/reviewPolicy/index.d.ts.map +1 -1
- package/dist/types/reviewPolicy/index.js +5 -1
- package/dist/types/reviewPolicy/index.js.map +1 -1
- package/dist-esm/database-lifecycle/index.d.ts +33 -74
- package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
- package/dist-esm/database-lifecycle/index.js +32 -29
- package/dist-esm/database-lifecycle/index.js.map +1 -1
- package/dist-esm/database-lifecycle/index.test.js +6 -6
- package/dist-esm/database-lifecycle/index.test.js.map +1 -1
- package/dist-esm/socket/protocol.d.ts +14 -1
- package/dist-esm/socket/protocol.d.ts.map +1 -1
- package/dist-esm/socket/protocol.js.map +1 -1
- package/dist-esm/types/ai/index.d.ts +1 -0
- package/dist-esm/types/ai/index.d.ts.map +1 -1
- package/dist-esm/types/ai/index.js +1 -0
- package/dist-esm/types/ai/index.js.map +1 -1
- package/dist-esm/types/ai/quota-paywall.d.ts +8 -0
- package/dist-esm/types/ai/quota-paywall.d.ts.map +1 -1
- package/dist-esm/types/ai/quota-paywall.js +17 -0
- package/dist-esm/types/ai/quota-paywall.js.map +1 -1
- package/dist-esm/types/ai/safety-classification.d.ts +16 -0
- package/dist-esm/types/ai/safety-classification.d.ts.map +1 -0
- package/dist-esm/types/ai/safety-classification.js +19 -0
- package/dist-esm/types/ai/safety-classification.js.map +1 -0
- package/dist-esm/types/audit/ocsf.d.ts +32 -0
- package/dist-esm/types/audit/ocsf.d.ts.map +1 -1
- package/dist-esm/types/audit/ocsf.js +2 -0
- package/dist-esm/types/audit/ocsf.js.map +1 -1
- package/dist-esm/types/billing/billing.d.ts +1 -0
- package/dist-esm/types/billing/billing.d.ts.map +1 -1
- package/dist-esm/types/billing/billing.js +9 -0
- package/dist-esm/types/billing/billing.js.map +1 -1
- package/dist-esm/types/billing/index.d.ts +1 -0
- package/dist-esm/types/billing/index.d.ts.map +1 -1
- package/dist-esm/types/billing/index.js +1 -0
- package/dist-esm/types/billing/index.js.map +1 -1
- package/dist-esm/types/billing/spendAlert.d.ts +62 -0
- package/dist-esm/types/billing/spendAlert.d.ts.map +1 -0
- package/dist-esm/types/billing/spendAlert.js +10 -0
- package/dist-esm/types/billing/spendAlert.js.map +1 -0
- package/dist-esm/types/policyGate/index.d.ts +24 -0
- package/dist-esm/types/policyGate/index.d.ts.map +1 -1
- package/dist-esm/types/policyGate/index.js +8 -1
- package/dist-esm/types/policyGate/index.js.map +1 -1
- package/dist-esm/types/rbac/index.d.ts +4 -0
- package/dist-esm/types/rbac/index.d.ts.map +1 -1
- package/dist-esm/types/rbac/index.js +4 -0
- package/dist-esm/types/rbac/index.js.map +1 -1
- package/dist-esm/types/reviewPolicy/index.d.ts +9 -3
- package/dist-esm/types/reviewPolicy/index.d.ts.map +1 -1
- package/dist-esm/types/reviewPolicy/index.js +4 -0
- package/dist-esm/types/reviewPolicy/index.js.map +1 -1
- package/package.json +2 -2
- package/src/database-lifecycle/index.test.ts +6 -6
- package/src/database-lifecycle/index.ts +76 -152
- package/src/socket/protocol.ts +19 -2
- package/src/types/ai/index.ts +1 -0
- package/src/types/ai/quota-paywall.ts +20 -0
- package/src/types/ai/safety-classification.ts +27 -0
- package/src/types/audit/ocsf.ts +43 -0
- package/src/types/billing/billing.ts +11 -0
- package/src/types/billing/index.ts +1 -0
- package/src/types/billing/spendAlert.ts +84 -0
- package/src/types/policyGate/index.ts +28 -0
- package/src/types/rbac/index.ts +4 -0
- package/src/types/reviewPolicy/index.ts +13 -3
|
@@ -4,12 +4,32 @@ export const LIFECYCLE_TERMINAL_STATES = ['ready', 'failed', 'cancelled'] as con
|
|
|
4
4
|
export const LIFECYCLE_NON_TERMINAL_STATES = ['pending', 'provisioning', 'migrating', 'retiring'] as const;
|
|
5
5
|
export const LIFECYCLE_STATES = [...LIFECYCLE_NON_TERMINAL_STATES, ...LIFECYCLE_TERMINAL_STATES] as const;
|
|
6
6
|
export const LIFECYCLE_MIGRATION_STATES = ['pending', 'migrated', 'failed'] as const;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// The environment axis (edit/preview/deployed) lives on the binding, so a
|
|
8
|
+
// single `ensure_database` covers what used to be split into
|
|
9
|
+
// ensure_dev_database / ensure_prod_database. `migrate_schema` and
|
|
10
|
+
// `retire_database` were always environment-agnostic.
|
|
11
|
+
export const LIFECYCLE_OPERATIONS = ['ensure_database', 'migrate_schema', 'retire_database'] as const;
|
|
12
|
+
// Mirrors the platform's view modes (proto api.v1.ViewMode: VIEW_MODE_EDIT /
|
|
13
|
+
// VIEW_MODE_PREVIEW / VIEW_MODE_DEPLOYED). Working-state migrations and the
|
|
14
|
+
// shared workspace database serve edit/preview; deploy-commit migrations and
|
|
15
|
+
// the deploy gate concern deployed.
|
|
16
|
+
export const LIFECYCLE_ENVIRONMENTS = ['edit', 'preview', 'deployed'] as const;
|
|
9
17
|
export const DATABASE_ENGINES = ['postgres', 'snowflake', 'snowflake_postgres', 'lakebase'] as const;
|
|
10
18
|
export const DATABASE_LIFECYCLE_MANAGED_BY = 'database_lifecycle';
|
|
11
19
|
|
|
12
|
-
|
|
20
|
+
// Agent capability tag keys. A lifecycle worker publishes these in the
|
|
21
|
+
// `tags` map of its agent registration (merged into — never replacing — the
|
|
22
|
+
// tag map the agent already publishes; the existing `profile` tag carries
|
|
23
|
+
// datatag coverage, same meaning it has for execution routing. Lifecycle
|
|
24
|
+
// environment/profile coverage is published as pairs so edit-only staging
|
|
25
|
+
// profiles do not imply deployed staging support. The server matches pending
|
|
26
|
+
// lifecycle requests against these at poll/claim time and gates task creation
|
|
27
|
+
// on "some active org agent supports this".
|
|
28
|
+
export const DATABASE_LIFECYCLE_TAG_OPERATIONS = 'databaseLifecycle:operations';
|
|
29
|
+
export const DATABASE_LIFECYCLE_TAG_ENGINES = 'databaseLifecycle:engines';
|
|
30
|
+
export const DATABASE_LIFECYCLE_TAG_ENVIRONMENT_PROFILES = 'databaseLifecycle:environmentProfiles';
|
|
31
|
+
|
|
32
|
+
export type LifecycleEnvironment = (typeof LIFECYCLE_ENVIRONMENTS)[number];
|
|
13
33
|
export type DatabaseEngine = (typeof DATABASE_ENGINES)[number];
|
|
14
34
|
export type LifecycleOperation = (typeof LIFECYCLE_OPERATIONS)[number];
|
|
15
35
|
export type LifecycleTerminalState = (typeof LIFECYCLE_TERMINAL_STATES)[number];
|
|
@@ -40,22 +60,6 @@ export type CredentialRef = {
|
|
|
40
60
|
field?: string;
|
|
41
61
|
};
|
|
42
62
|
|
|
43
|
-
export type CredentialResolverConfig = {
|
|
44
|
-
type: CredentialResolver;
|
|
45
|
-
config: Record<string, unknown>;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Reference to a versioned Terraform module that the lifecycle worker should
|
|
49
|
-
// invoke for one operation in this profile. `baseInputs` are profile-level
|
|
50
|
-
// inputs the org admin sets once (AWS account/region/VPC/subnets/KMS/...);
|
|
51
|
-
// the planner merges these with binding-derived inputs (binding_key,
|
|
52
|
-
// requirement spec, credential refs) when it builds a dispatch payload.
|
|
53
|
-
export type TerraformModuleRef = {
|
|
54
|
-
source: string;
|
|
55
|
-
version: string;
|
|
56
|
-
baseInputs: Record<string, unknown>;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
63
|
export type DatabaseRequirement = {
|
|
60
64
|
logicalName: string;
|
|
61
65
|
engine: DatabaseEngine;
|
|
@@ -66,51 +70,17 @@ export type DatabaseRequirement = {
|
|
|
66
70
|
migrationDirectory?: string;
|
|
67
71
|
};
|
|
68
72
|
|
|
69
|
-
export type TerraformDatabaseBackend = {
|
|
70
|
-
provisioner: 'terraform';
|
|
71
|
-
provider: 'aws-rds' | 'snowflake' | 'databricks';
|
|
72
|
-
stateBackend: 's3' | 'gcs' | 'azurerm' | 'local';
|
|
73
|
-
remoteState: boolean;
|
|
74
|
-
locking: boolean;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export type DatabaseBackend = TerraformDatabaseBackend;
|
|
78
|
-
|
|
79
|
-
export type EnvironmentProfile = {
|
|
80
|
-
id: string;
|
|
81
|
-
organizationId: string;
|
|
82
|
-
environmentClass: EnvironmentClass;
|
|
83
|
-
environmentName: string;
|
|
84
|
-
opaAgentId: string;
|
|
85
|
-
supportedOperations: LifecycleOperation[];
|
|
86
|
-
supportedEngines: DatabaseEngine[];
|
|
87
|
-
backend: DatabaseBackend;
|
|
88
|
-
// V1 stop-gap. Picks the Terraform module the lifecycle worker invokes
|
|
89
|
-
// per operation. `Partial` because not every operation is Terraform-backed
|
|
90
|
-
// in V1 — `migrate_schema` is served by the native Go runner inside the
|
|
91
|
-
// worker (ENG-3415); admins leave it unset and the dispatcher skips
|
|
92
|
-
// emitting a Terraform dispatch for it.
|
|
93
|
-
//
|
|
94
|
-
// TODO(ENG-3456): collapse into per-operation backends when the
|
|
95
|
-
// shared-vs-isolated admin config lands. Each operation will then carry
|
|
96
|
-
// its own discriminated kind ('terraform' | 'native-migration-runner'),
|
|
97
|
-
// with the module reference and Terraform backend config nested under
|
|
98
|
-
// kind: 'terraform'. The wire protocol picks up a matching discriminant.
|
|
99
|
-
moduleSelectors: Partial<Record<LifecycleOperation, TerraformModuleRef>>;
|
|
100
|
-
// Where the worker resolves and writes credential refs. Scoped per profile
|
|
101
|
-
// because dev/prod profiles may use different resolvers (e.g. opa_local
|
|
102
|
-
// for dev, aws_secrets_manager for prod).
|
|
103
|
-
credentialResolver: CredentialResolverConfig;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
73
|
type DatabaseBindingBase = {
|
|
107
74
|
id: string;
|
|
108
75
|
bindingKey: string;
|
|
109
76
|
requirementKey: string;
|
|
110
77
|
logicalName: string;
|
|
111
78
|
applicationId: string;
|
|
112
|
-
|
|
113
|
-
|
|
79
|
+
environment: LifecycleEnvironment;
|
|
80
|
+
// The datatag key (Profile.key, e.g. 'staging' / 'production') this
|
|
81
|
+
// binding serves. The same datatag can exist across environments, so the
|
|
82
|
+
// pair (environment, profile) — not either alone — scopes a binding.
|
|
83
|
+
profile: string;
|
|
114
84
|
desiredSpecHash: string;
|
|
115
85
|
migrationState: LifecycleMigrationState;
|
|
116
86
|
};
|
|
@@ -148,47 +118,33 @@ export type LifecycleRequest = {
|
|
|
148
118
|
state: LifecycleState;
|
|
149
119
|
};
|
|
150
120
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
export type TerraformModuleOutput = {
|
|
159
|
-
connection: Record<string, string | number | boolean>;
|
|
160
|
-
credentialRefs: Record<string, CredentialRef>;
|
|
161
|
-
resourceKey: string;
|
|
162
|
-
};
|
|
121
|
+
// Physical database instances back the M2 shared-RDS allocation pattern: dev-DB
|
|
122
|
+
// provisioning issues `CREATE DATABASE`/`CREATE ROLE` against a pre-existing
|
|
123
|
+
// physical database instance instead of spinning up a fresh RDS per binding.
|
|
124
|
+
// The control plane is the dumb org-scoped state store (registry + atomic
|
|
125
|
+
// capacity counter); ALL selection and provisioning logic lives in the worker.
|
|
126
|
+
export const PHYSICAL_DATABASE_INSTANCE_STATUSES = ['active', 'draining', 'retired'] as const;
|
|
127
|
+
export type PhysicalDatabaseInstanceStatus = (typeof PHYSICAL_DATABASE_INSTANCE_STATUSES)[number];
|
|
163
128
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// `terraform { backend "<stateBackend>" {} }` block — same name as on the
|
|
179
|
-
// profile side. `remoteState` and `locking` pass through unchanged. The
|
|
180
|
-
// worker doesn't see `provisioner`/`provider` — those are server-side
|
|
181
|
-
// categorical fields, not backend args.
|
|
182
|
-
export type TerraformBackendDispatch = {
|
|
183
|
-
stateBackend: TerraformDatabaseBackend['stateBackend'];
|
|
184
|
-
remoteState: boolean;
|
|
185
|
-
locking: boolean;
|
|
186
|
-
key: string;
|
|
129
|
+
export type PhysicalDatabaseInstance = {
|
|
130
|
+
id: string;
|
|
131
|
+
organizationId: string;
|
|
132
|
+
region: string;
|
|
133
|
+
environment: LifecycleEnvironment;
|
|
134
|
+
engine: DatabaseEngine; // postgres-only in V1
|
|
135
|
+
endpoint: string;
|
|
136
|
+
masterCredentialRef: CredentialRef;
|
|
137
|
+
capacityMax: number;
|
|
138
|
+
capacityUsed: number;
|
|
139
|
+
status: PhysicalDatabaseInstanceStatus;
|
|
140
|
+
metadata: Record<string, unknown>;
|
|
141
|
+
created?: Date;
|
|
142
|
+
updated?: Date;
|
|
187
143
|
};
|
|
188
144
|
|
|
189
145
|
// One forward-only SQL migration the server attaches to a dispatch
|
|
190
146
|
// payload so the lifecycle worker's migration runner can apply it after
|
|
191
|
-
//
|
|
147
|
+
// provisioning succeeds. `version` is the sort key + the primary key in
|
|
192
148
|
// the worker's in-DB `superblocks_schema_migrations` ledger; `filename`
|
|
193
149
|
// is recorded for diagnostics; `sql` is the raw multi-statement SQL.
|
|
194
150
|
// Matches `orchestrator/pkg/databaselifecycle/migrations.Migration`.
|
|
@@ -201,32 +157,31 @@ export type LifecycleMigration = {
|
|
|
201
157
|
// Canonical wire payload for a lifecycle dispatch sent from the server to a
|
|
202
158
|
// lifecycle worker. The worker's `DispatchPayload` struct in
|
|
203
159
|
// orchestrator/pkg/databaselifecycle/dispatch.go decodes this JSON shape;
|
|
204
|
-
// keys and order here are intentional.
|
|
205
|
-
//
|
|
160
|
+
// keys and order here are intentional.
|
|
161
|
+
//
|
|
162
|
+
// The server describes WHAT (binding identity, desired spec, migrations,
|
|
163
|
+
// connection/credential context); the worker owns HOW (Terraform modules,
|
|
164
|
+
// state backends, credential resolvers, shared physical database instances — all resolved from
|
|
165
|
+
// the worker's local config keyed by the payload's environment + profile).
|
|
206
166
|
//
|
|
207
167
|
// `migrations` is present iff the operation should consider migration
|
|
208
|
-
// state — `
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
// "
|
|
212
|
-
// non-empty slice triggers the runner.
|
|
168
|
+
// state — `ensure_database` / `migrate_schema` dispatches carry an array
|
|
169
|
+
// (possibly empty); `retire_database` omits it. An omitted slice keeps the
|
|
170
|
+
// worker's `MigrationState` at the default "pending"; an empty slice means
|
|
171
|
+
// "vacuous truth, mark migrated"; a non-empty slice triggers the runner.
|
|
213
172
|
export type LifecycleDispatchPayload = {
|
|
214
|
-
agentId: string;
|
|
215
173
|
bindingKey: string;
|
|
216
174
|
connectionMetadata?: Record<string, string | number | boolean>;
|
|
217
175
|
desiredSpec: DatabaseRequirement;
|
|
218
176
|
desiredSpecHash: string;
|
|
177
|
+
environment: LifecycleEnvironment;
|
|
219
178
|
migrations?: LifecycleMigration[];
|
|
220
179
|
operation: LifecycleOperation;
|
|
221
|
-
|
|
180
|
+
profile: string;
|
|
222
181
|
requestId: string;
|
|
223
182
|
resourceKey: string;
|
|
224
183
|
runtimeCredentialRefs?: Record<string, CredentialRef>;
|
|
225
|
-
|
|
226
|
-
// dispatches, where the worker skips Terraform materialization and runs the
|
|
227
|
-
// migration runner directly.
|
|
228
|
-
terraformBackend?: TerraformBackendDispatch;
|
|
229
|
-
terraformModule?: TerraformModuleDispatch;
|
|
184
|
+
migrationCredentialRefs?: Record<string, CredentialRef>;
|
|
230
185
|
};
|
|
231
186
|
|
|
232
187
|
export function computeRequirementKey(requirement: Pick<DatabaseRequirement, 'logicalName' | 'engine'>): string {
|
|
@@ -236,8 +191,8 @@ export function computeRequirementKey(requirement: Pick<DatabaseRequirement, 'lo
|
|
|
236
191
|
export function computeBindingKey(input: {
|
|
237
192
|
organizationId: string;
|
|
238
193
|
applicationId: string;
|
|
239
|
-
|
|
240
|
-
|
|
194
|
+
environment: LifecycleEnvironment;
|
|
195
|
+
profile: string;
|
|
241
196
|
requirementKey: string;
|
|
242
197
|
}): string {
|
|
243
198
|
return [input.organizationId, ...bindingKeySegments(input)].join(':');
|
|
@@ -245,8 +200,8 @@ export function computeBindingKey(input: {
|
|
|
245
200
|
|
|
246
201
|
export function computeLegacyBindingKeyWithoutOrganization(input: {
|
|
247
202
|
applicationId: string;
|
|
248
|
-
|
|
249
|
-
|
|
203
|
+
environment: LifecycleEnvironment;
|
|
204
|
+
profile: string;
|
|
250
205
|
requirementKey: string;
|
|
251
206
|
}): string {
|
|
252
207
|
return bindingKeySegments(input).join(':');
|
|
@@ -254,16 +209,11 @@ export function computeLegacyBindingKeyWithoutOrganization(input: {
|
|
|
254
209
|
|
|
255
210
|
function bindingKeySegments(input: {
|
|
256
211
|
applicationId: string;
|
|
257
|
-
|
|
258
|
-
|
|
212
|
+
environment: LifecycleEnvironment;
|
|
213
|
+
profile: string;
|
|
259
214
|
requirementKey: string;
|
|
260
215
|
}): string[] {
|
|
261
|
-
return [
|
|
262
|
-
input.applicationId,
|
|
263
|
-
input.environmentClass,
|
|
264
|
-
`${slugify(input.environmentName)}~${encodeURIComponent(input.environmentName)}`,
|
|
265
|
-
input.requirementKey
|
|
266
|
-
];
|
|
216
|
+
return [input.applicationId, input.environment, `${slugify(input.profile)}~${encodeURIComponent(input.profile)}`, input.requirementKey];
|
|
267
217
|
}
|
|
268
218
|
|
|
269
219
|
export async function computeDesiredSpecHash(requirement: DatabaseRequirement): Promise<string> {
|
|
@@ -273,52 +223,26 @@ export async function computeDesiredSpecHash(requirement: DatabaseRequirement):
|
|
|
273
223
|
// resource_key identifies the physical resource a binding maps to in
|
|
274
224
|
// customer infrastructure, and is the unit of locking inside the lifecycle
|
|
275
225
|
// worker. Distinct from binding_key (product identity in the control plane)
|
|
276
|
-
// because
|
|
277
|
-
//
|
|
278
|
-
// fresh resource. Shape mirrors planning doc §15.
|
|
226
|
+
// because the worker derives infrastructure identity from it — shape
|
|
227
|
+
// mirrors planning doc §15.
|
|
279
228
|
export function computeResourceKey(input: {
|
|
280
229
|
organizationId: string;
|
|
281
|
-
profileId: string;
|
|
282
230
|
applicationId: string;
|
|
283
231
|
requirementKey: string;
|
|
284
|
-
|
|
285
|
-
|
|
232
|
+
environment: LifecycleEnvironment;
|
|
233
|
+
profile: string;
|
|
286
234
|
actorScope?: string;
|
|
287
235
|
}): string {
|
|
288
236
|
return [
|
|
289
237
|
input.organizationId,
|
|
290
|
-
input.profileId,
|
|
291
238
|
input.applicationId,
|
|
292
239
|
input.requirementKey,
|
|
293
|
-
input.
|
|
294
|
-
`${slugify(input.
|
|
240
|
+
input.environment,
|
|
241
|
+
`${slugify(input.profile)}~${encodeURIComponent(input.profile)}`,
|
|
295
242
|
input.actorScope ?? 'default'
|
|
296
243
|
].join('/');
|
|
297
244
|
}
|
|
298
245
|
|
|
299
|
-
// Per-binding Terraform state path inside the configured backend. Used to
|
|
300
|
-
// generate the `backend.tfbackend` config that the worker passes to
|
|
301
|
-
// `terraform init -backend-config=...`. Planning doc §9.4.
|
|
302
|
-
export function computeTerraformStateKey(input: {
|
|
303
|
-
organizationId: string;
|
|
304
|
-
applicationId: string;
|
|
305
|
-
requirementKey: string;
|
|
306
|
-
environmentClass: EnvironmentClass;
|
|
307
|
-
environmentName: string;
|
|
308
|
-
actorScope?: string;
|
|
309
|
-
}): string {
|
|
310
|
-
return [
|
|
311
|
-
'superblocks/byo-db',
|
|
312
|
-
input.environmentClass,
|
|
313
|
-
input.organizationId,
|
|
314
|
-
input.applicationId,
|
|
315
|
-
input.requirementKey,
|
|
316
|
-
`${slugify(input.environmentName)}~${encodeURIComponent(input.environmentName)}`,
|
|
317
|
-
input.actorScope ?? 'default',
|
|
318
|
-
'terraform.tfstate'
|
|
319
|
-
].join('/');
|
|
320
|
-
}
|
|
321
|
-
|
|
322
246
|
export function isTerminalLifecycleState(state: LifecycleState): state is LifecycleTerminalState {
|
|
323
247
|
return (LIFECYCLE_TERMINAL_STATES as readonly string[]).includes(state);
|
|
324
248
|
}
|
package/src/socket/protocol.ts
CHANGED
|
@@ -37,7 +37,8 @@ import {
|
|
|
37
37
|
FactCreate,
|
|
38
38
|
FactDto,
|
|
39
39
|
FactListQuery,
|
|
40
|
-
ListFactsResponse
|
|
40
|
+
ListFactsResponse,
|
|
41
|
+
NpmInstallBlockedAuditReport
|
|
41
42
|
} from '../types/index.js';
|
|
42
43
|
import { MethodSchema } from './types.js';
|
|
43
44
|
|
|
@@ -160,6 +161,15 @@ export interface ServerMethods {
|
|
|
160
161
|
list: ServerMethodSchema<FactListQuery, ListFactsResponse>;
|
|
161
162
|
create: ServerMethodSchema<FactCreate, FactDto>;
|
|
162
163
|
};
|
|
164
|
+
audit: {
|
|
165
|
+
/**
|
|
166
|
+
* Report a controlled-install `NpmInstallBlocked` so the server can write a
|
|
167
|
+
* throttled OCSF audit row (APPS-4191 / P6.3). Org + actor are derived from
|
|
168
|
+
* the authenticated connection; `recorded` is false when the event was
|
|
169
|
+
* dropped by the per-org or global throttle.
|
|
170
|
+
*/
|
|
171
|
+
npmInstallBlocked: ServerMethodSchema<NpmInstallBlockedAuditReport, { recorded: boolean }>;
|
|
172
|
+
};
|
|
163
173
|
};
|
|
164
174
|
v2: {
|
|
165
175
|
application: {
|
|
@@ -215,7 +225,14 @@ export interface ServerMethods {
|
|
|
215
225
|
*/
|
|
216
226
|
get: ServerMethodSchema<{ applicationId: string; branchName?: string }, { hash: string }>;
|
|
217
227
|
set: ServerMethodSchema<
|
|
218
|
-
{
|
|
228
|
+
{
|
|
229
|
+
applicationId: string;
|
|
230
|
+
branchName?: string;
|
|
231
|
+
hash: string;
|
|
232
|
+
source?: string;
|
|
233
|
+
targetTemplateName?: string;
|
|
234
|
+
migrationGeneration?: number;
|
|
235
|
+
},
|
|
219
236
|
{ hash: string; degradedMode?: DegradedMode; commitId?: string; checkpointSkipped?: CheckpointSkipReason }
|
|
220
237
|
>;
|
|
221
238
|
};
|
package/src/types/ai/index.ts
CHANGED
|
@@ -8,6 +8,26 @@ export type AiQuotaPaywallReason =
|
|
|
8
8
|
| 'trial_expired'
|
|
9
9
|
| 'user_credit_limit_exceeded';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The complete set of paywall reasons, kept in lockstep with the
|
|
13
|
+
* `AiQuotaPaywallReason` union above. Shared so that callers parsing
|
|
14
|
+
* untrusted error payloads (`packages/vite-plugin-file-sync`) can validate
|
|
15
|
+
* string values against the same source of truth rather than duplicating it.
|
|
16
|
+
*/
|
|
17
|
+
export const AI_QUOTA_PAYWALL_REASONS: ReadonlySet<AiQuotaPaywallReason> = new Set<AiQuotaPaywallReason>([
|
|
18
|
+
'credit_limit_exceeded',
|
|
19
|
+
'deploy_quota_exceeded',
|
|
20
|
+
'dollar_commit_exhausted',
|
|
21
|
+
'no_seat_assigned',
|
|
22
|
+
'payment_past_due',
|
|
23
|
+
'token_limit_exceeded',
|
|
24
|
+
'trial_expired',
|
|
25
|
+
'user_credit_limit_exceeded'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export const isAiQuotaPaywallReason = (value: unknown): value is AiQuotaPaywallReason =>
|
|
29
|
+
typeof value === 'string' && AI_QUOTA_PAYWALL_REASONS.has(value as AiQuotaPaywallReason);
|
|
30
|
+
|
|
11
31
|
export const getAiQuotaPaywallReasonFromMessage = (message: string): AiQuotaPaywallReason | undefined => {
|
|
12
32
|
const normalizedMessage = message.toLowerCase();
|
|
13
33
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants and types for LLM-based prompt safety classification.
|
|
3
|
+
*
|
|
4
|
+
* Used by the Clark AI service (vite-plugin-file-sync) and the server-side
|
|
5
|
+
* policy-gate prompt check. Each consumer owns its own context-specific
|
|
6
|
+
* system prompt; only the structural contract (categories, result types,
|
|
7
|
+
* default results) lives here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const SAFETY_CATEGORIES = [
|
|
11
|
+
'harmful_instructions',
|
|
12
|
+
'illegal_activity',
|
|
13
|
+
'jailbreak_attempt',
|
|
14
|
+
'malicious_code',
|
|
15
|
+
'other',
|
|
16
|
+
'personal_data_extraction',
|
|
17
|
+
'prompt_injection',
|
|
18
|
+
'system_extraction'
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type SafetyCategory = (typeof SAFETY_CATEGORIES)[number];
|
|
22
|
+
|
|
23
|
+
export interface SafetyClassificationResult {
|
|
24
|
+
safe: boolean;
|
|
25
|
+
justification: string;
|
|
26
|
+
categories?: SafetyCategory[];
|
|
27
|
+
}
|
package/src/types/audit/ocsf.ts
CHANGED
|
@@ -163,6 +163,7 @@ export const AUDIT_EVENT_TYPE_CATALOG: AuditEventTypeEntry[] = [
|
|
|
163
163
|
'application.git.connect',
|
|
164
164
|
'application.git.disconnect',
|
|
165
165
|
'application.metadata.update',
|
|
166
|
+
'application.policy_gate_check.start',
|
|
166
167
|
'application.settings.update',
|
|
167
168
|
'application.undeploy'
|
|
168
169
|
]
|
|
@@ -237,6 +238,7 @@ export const AUDIT_EVENT_TYPE_CATALOG: AuditEventTypeEntry[] = [
|
|
|
237
238
|
resource_type: 'Organization',
|
|
238
239
|
operations: [
|
|
239
240
|
'organization.npm_allow_install_scripts_changed',
|
|
241
|
+
'organization.npm_install.blocked',
|
|
240
242
|
'organization.npm_registry.create',
|
|
241
243
|
'organization.npm_registry.delete',
|
|
242
244
|
'organization.npm_registry.update',
|
|
@@ -339,3 +341,44 @@ const METADATA: Omit<OCSFMetadata, 'log_name'> = {
|
|
|
339
341
|
export function buildOCSFMetadata(logName: string): OCSFMetadata {
|
|
340
342
|
return { ...METADATA, log_name: logName };
|
|
341
343
|
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// npm install-blocked audit report (APPS-4191 / P6.3)
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Wire payload for the `v1.audit.npmInstallBlocked` socket method.
|
|
351
|
+
*
|
|
352
|
+
* Emitted by the Clark dev-server runtime when the controlled install path
|
|
353
|
+
* reports `NpmInstallBlocked`. The server derives org + actor from the
|
|
354
|
+
* authenticated connection (never trusting the client for identity),
|
|
355
|
+
* throttles per-org and globally, sanitizes via the P6.1 telemetry helpers,
|
|
356
|
+
* and writes an OCSF `audit_event` row with operation
|
|
357
|
+
* `organization.npm_install.blocked`.
|
|
358
|
+
*
|
|
359
|
+
* Carries only structured, low-risk fields — deliberately NO free-text error
|
|
360
|
+
* message, so a registry token embedded in CLI output can never reach the wire.
|
|
361
|
+
*/
|
|
362
|
+
export interface NpmInstallBlockedAuditReport {
|
|
363
|
+
/** `NpmInstallBlocked.reason`; normalized to the shared npm outcome enum server-side. */
|
|
364
|
+
reason: string;
|
|
365
|
+
/** Raw registry host; the server buckets it to public_npm | private | unknown. */
|
|
366
|
+
registryHost?: string;
|
|
367
|
+
/** Requested package names; the server sanitizes each per P6.1 and caps the array. */
|
|
368
|
+
packages: string[];
|
|
369
|
+
/** HTTP status from the failed registry fetch, when known. */
|
|
370
|
+
httpStatus?: number;
|
|
371
|
+
/** Underlying npm/pnpm error code (e.g. E404), when known. Server caps length. */
|
|
372
|
+
npmErrorCode?: string;
|
|
373
|
+
/**
|
|
374
|
+
* Which controlled install path produced the block. The server allowlists
|
|
375
|
+
* against `NPM_INSTALL_RUNNERS` and drops any other value.
|
|
376
|
+
*/
|
|
377
|
+
runner?: string;
|
|
378
|
+
/** Epoch ms when the block occurred (client clock). */
|
|
379
|
+
occurredAt: number;
|
|
380
|
+
// application attribution: NOT a wire field. The server derives it from the
|
|
381
|
+
// trusted scoped-JWT claim (`ctx.jwtClaims.app_id`) — a payload-controlled
|
|
382
|
+
// value would let any authenticated caller forge audit rows against another
|
|
383
|
+
// app in the same org.
|
|
384
|
+
}
|
|
@@ -22,10 +22,21 @@ const ENTERPRISE_LIKE_PLANS: ReadonlySet<BillingPlan> = new Set([
|
|
|
22
22
|
|
|
23
23
|
const TRIAL_LIKE_PLANS: ReadonlySet<BillingPlan> = new Set([BillingPlan.TRIAL, BillingPlan.FREE]);
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Plans entitled to the Policy Gates MVP. The MVP rollout is limited to
|
|
27
|
+
* enterprise (and POC) orgs, so this is intentionally narrower than
|
|
28
|
+
* {@link isEnterpriseLikePlan} (which also covers legacy PRO/STARTER).
|
|
29
|
+
*/
|
|
30
|
+
const POLICY_GATES_ENTITLED_PLANS: ReadonlySet<BillingPlan> = new Set([BillingPlan.ENTERPRISE, BillingPlan.POC]);
|
|
31
|
+
|
|
25
32
|
export function isEnterpriseLikePlan(plan: BillingPlan | undefined | null): boolean {
|
|
26
33
|
return plan != null && ENTERPRISE_LIKE_PLANS.has(plan);
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
export function isPolicyGatesEntitledPlan(plan: BillingPlan | undefined | null): boolean {
|
|
37
|
+
return plan != null && POLICY_GATES_ENTITLED_PLANS.has(plan);
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
export function isTrialLikePlan(plan: BillingPlan | undefined | null): boolean {
|
|
30
41
|
return plan != null && TRIAL_LIKE_PLANS.has(plan);
|
|
31
42
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spend Alerts — proactive billing alerts admins configure on /spending.
|
|
3
|
+
*
|
|
4
|
+
* Wire types shared between client (RTK Query slice in reduxApi/billing) and
|
|
5
|
+
* server (/v1/billing/spend-alerts controller). The DB stores `thresholds`
|
|
6
|
+
* and `recipients` as JSONB blobs whose shape matches these types exactly —
|
|
7
|
+
* see migration 1778896201000-create-spend-alert-table.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type SpendAlertType = 'org_spend' | 'per_user_spend' | 'overage_spend' | 'refill_spend';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Unit identifier stored in `threshold.unit`. The client-side modal maps
|
|
14
|
+
* these to human labels (% of monthly usage / credits used / etc.); the
|
|
15
|
+
* server treats them opaquely for v1 (no evaluation yet).
|
|
16
|
+
*
|
|
17
|
+
* Adding a new unit requires an update here AND on the server-side enum
|
|
18
|
+
* in `validateThresholdUnit` so persistence rejects unknown values rather
|
|
19
|
+
* than silently storing them.
|
|
20
|
+
*/
|
|
21
|
+
export type SpendAlertThresholdUnit =
|
|
22
|
+
// Org spend
|
|
23
|
+
| 'percent'
|
|
24
|
+
| 'credits_used'
|
|
25
|
+
| 'dollars_spent'
|
|
26
|
+
// Per-user spend
|
|
27
|
+
| 'credits'
|
|
28
|
+
| 'dollars'
|
|
29
|
+
// Overage spend
|
|
30
|
+
| 'overage_credits'
|
|
31
|
+
| 'overage_dollars'
|
|
32
|
+
// Refill spend. Credit-denominated — every refill-eligible plan
|
|
33
|
+
// (TEAMS / POC) is credit-based, so there's no dollar variant.
|
|
34
|
+
| 'refill_credits';
|
|
35
|
+
|
|
36
|
+
export interface SpendAlertThreshold {
|
|
37
|
+
/** Positive number. Server validates this is > 0. */
|
|
38
|
+
value: number;
|
|
39
|
+
unit: SpendAlertThresholdUnit;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SpendAlertRecipientKind =
|
|
43
|
+
/** Everyone with admin role on the org. Resolved at dispatch time. */
|
|
44
|
+
| 'all_admins'
|
|
45
|
+
/**
|
|
46
|
+
* Per-user alerts only. The user whose usage crossed the threshold.
|
|
47
|
+
* Resolved at dispatch time — not stored as a user_id, since the
|
|
48
|
+
* recipient depends on which user fired the alert.
|
|
49
|
+
*/
|
|
50
|
+
| 'triggering_user'
|
|
51
|
+
/** Specific user by id. Resolved against the user table at dispatch. */
|
|
52
|
+
| 'user';
|
|
53
|
+
|
|
54
|
+
export interface SpendAlertRecipient {
|
|
55
|
+
kind: SpendAlertRecipientKind;
|
|
56
|
+
/** Required when kind === 'user'. Ignored for other kinds. */
|
|
57
|
+
userId?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SpendAlertDto {
|
|
61
|
+
id: string;
|
|
62
|
+
organizationId: string;
|
|
63
|
+
alertType: SpendAlertType;
|
|
64
|
+
thresholds: SpendAlertThreshold[];
|
|
65
|
+
recipients: SpendAlertRecipient[];
|
|
66
|
+
created: string;
|
|
67
|
+
updated: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CreateSpendAlertBody {
|
|
71
|
+
alertType: SpendAlertType;
|
|
72
|
+
thresholds: SpendAlertThreshold[];
|
|
73
|
+
recipients: SpendAlertRecipient[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type UpdateSpendAlertBody = Partial<Pick<CreateSpendAlertBody, 'thresholds' | 'recipients'>>;
|
|
77
|
+
|
|
78
|
+
export interface ListSpendAlertsResponseBody {
|
|
79
|
+
alerts: SpendAlertDto[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SpendAlertResponseBody {
|
|
83
|
+
alert: SpendAlertDto;
|
|
84
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Single source of truth for the built-in security scan surfaces; flip to `true` to restore the security-scan UI and execution.
|
|
2
|
+
export const SECURITY_SCANS_ENABLED: boolean = false;
|
|
3
|
+
|
|
1
4
|
export type PolicyGateReadinessAction =
|
|
2
5
|
| 'contact_admin'
|
|
3
6
|
| 'fix_with_clark'
|
|
@@ -16,7 +19,11 @@ export type PolicyGateReadinessFindingSummary = {
|
|
|
16
19
|
|
|
17
20
|
export type PolicyGateReadinessFinding = {
|
|
18
21
|
blocking: boolean;
|
|
22
|
+
humanSummary?: string | null;
|
|
23
|
+
locationsJson?: unknown[];
|
|
24
|
+
remediationHintJson?: Record<string, unknown>;
|
|
19
25
|
severity: 'critical' | 'high' | 'info' | 'low' | 'medium';
|
|
26
|
+
technicalSummary?: string | null;
|
|
20
27
|
title: string;
|
|
21
28
|
};
|
|
22
29
|
|
|
@@ -48,10 +55,24 @@ export type PolicyGateReadinessItem = {
|
|
|
48
55
|
policyVersionId: string;
|
|
49
56
|
progress?: PolicyGateScanProgress;
|
|
50
57
|
reviewRunId?: string;
|
|
58
|
+
runs?: PolicyGateReadinessRun[];
|
|
51
59
|
staleReason?: string;
|
|
52
60
|
status: PolicyGateReadinessItemStatus;
|
|
53
61
|
};
|
|
54
62
|
|
|
63
|
+
export type PolicyGateReadinessRun = {
|
|
64
|
+
completedAt?: string;
|
|
65
|
+
createdAt?: string;
|
|
66
|
+
decision: 'advisory_allowed' | 'allowed' | 'blocked' | 'error_blocked' | 'not_applicable' | null;
|
|
67
|
+
errorCode?: string;
|
|
68
|
+
errorMessage?: string;
|
|
69
|
+
findingSummary: PolicyGateReadinessFindingSummary;
|
|
70
|
+
reviewRunId: string;
|
|
71
|
+
startedAt?: string;
|
|
72
|
+
staleReason?: string | null;
|
|
73
|
+
status: PolicyGateReadinessItemStatus;
|
|
74
|
+
};
|
|
75
|
+
|
|
55
76
|
export type PolicyGateReadinessItemStatus =
|
|
56
77
|
| 'advisory_findings'
|
|
57
78
|
| 'approval_required'
|
|
@@ -75,3 +96,10 @@ export type PolicyGateReadinessTarget = {
|
|
|
75
96
|
commitId: string;
|
|
76
97
|
directoryContentsHash: string | null;
|
|
77
98
|
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Stable error code used in BadRequestError messages when a deploy is rejected
|
|
102
|
+
* due to unresolved policy gate findings. Both server and client reference this
|
|
103
|
+
* constant so the contract doesn't rely on fragile string matching.
|
|
104
|
+
*/
|
|
105
|
+
export const POLICY_GATE_BLOCKED_ERROR = 'POLICY_GATE_BLOCKED';
|