@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.
Files changed (112) hide show
  1. package/dist/database-lifecycle/index.d.ts +33 -74
  2. package/dist/database-lifecycle/index.d.ts.map +1 -1
  3. package/dist/database-lifecycle/index.js +33 -31
  4. package/dist/database-lifecycle/index.js.map +1 -1
  5. package/dist/database-lifecycle/index.test.js +6 -6
  6. package/dist/database-lifecycle/index.test.js.map +1 -1
  7. package/dist/socket/protocol.d.ts +14 -1
  8. package/dist/socket/protocol.d.ts.map +1 -1
  9. package/dist/socket/protocol.js.map +1 -1
  10. package/dist/types/ai/index.d.ts +1 -0
  11. package/dist/types/ai/index.d.ts.map +1 -1
  12. package/dist/types/ai/index.js +1 -0
  13. package/dist/types/ai/index.js.map +1 -1
  14. package/dist/types/ai/quota-paywall.d.ts +8 -0
  15. package/dist/types/ai/quota-paywall.d.ts.map +1 -1
  16. package/dist/types/ai/quota-paywall.js +19 -1
  17. package/dist/types/ai/quota-paywall.js.map +1 -1
  18. package/dist/types/ai/safety-classification.d.ts +16 -0
  19. package/dist/types/ai/safety-classification.d.ts.map +1 -0
  20. package/dist/types/ai/safety-classification.js +22 -0
  21. package/dist/types/ai/safety-classification.js.map +1 -0
  22. package/dist/types/audit/ocsf.d.ts +32 -0
  23. package/dist/types/audit/ocsf.d.ts.map +1 -1
  24. package/dist/types/audit/ocsf.js +2 -0
  25. package/dist/types/audit/ocsf.js.map +1 -1
  26. package/dist/types/billing/billing.d.ts +1 -0
  27. package/dist/types/billing/billing.d.ts.map +1 -1
  28. package/dist/types/billing/billing.js +10 -0
  29. package/dist/types/billing/billing.js.map +1 -1
  30. package/dist/types/billing/index.d.ts +1 -0
  31. package/dist/types/billing/index.d.ts.map +1 -1
  32. package/dist/types/billing/index.js +1 -0
  33. package/dist/types/billing/index.js.map +1 -1
  34. package/dist/types/billing/spendAlert.d.ts +62 -0
  35. package/dist/types/billing/spendAlert.d.ts.map +1 -0
  36. package/dist/types/billing/spendAlert.js +11 -0
  37. package/dist/types/billing/spendAlert.js.map +1 -0
  38. package/dist/types/policyGate/index.d.ts +24 -0
  39. package/dist/types/policyGate/index.d.ts.map +1 -1
  40. package/dist/types/policyGate/index.js +9 -0
  41. package/dist/types/policyGate/index.js.map +1 -1
  42. package/dist/types/rbac/index.d.ts +4 -0
  43. package/dist/types/rbac/index.d.ts.map +1 -1
  44. package/dist/types/rbac/index.js +4 -0
  45. package/dist/types/rbac/index.js.map +1 -1
  46. package/dist/types/reviewPolicy/index.d.ts +9 -3
  47. package/dist/types/reviewPolicy/index.d.ts.map +1 -1
  48. package/dist/types/reviewPolicy/index.js +5 -1
  49. package/dist/types/reviewPolicy/index.js.map +1 -1
  50. package/dist-esm/database-lifecycle/index.d.ts +33 -74
  51. package/dist-esm/database-lifecycle/index.d.ts.map +1 -1
  52. package/dist-esm/database-lifecycle/index.js +32 -29
  53. package/dist-esm/database-lifecycle/index.js.map +1 -1
  54. package/dist-esm/database-lifecycle/index.test.js +6 -6
  55. package/dist-esm/database-lifecycle/index.test.js.map +1 -1
  56. package/dist-esm/socket/protocol.d.ts +14 -1
  57. package/dist-esm/socket/protocol.d.ts.map +1 -1
  58. package/dist-esm/socket/protocol.js.map +1 -1
  59. package/dist-esm/types/ai/index.d.ts +1 -0
  60. package/dist-esm/types/ai/index.d.ts.map +1 -1
  61. package/dist-esm/types/ai/index.js +1 -0
  62. package/dist-esm/types/ai/index.js.map +1 -1
  63. package/dist-esm/types/ai/quota-paywall.d.ts +8 -0
  64. package/dist-esm/types/ai/quota-paywall.d.ts.map +1 -1
  65. package/dist-esm/types/ai/quota-paywall.js +17 -0
  66. package/dist-esm/types/ai/quota-paywall.js.map +1 -1
  67. package/dist-esm/types/ai/safety-classification.d.ts +16 -0
  68. package/dist-esm/types/ai/safety-classification.d.ts.map +1 -0
  69. package/dist-esm/types/ai/safety-classification.js +19 -0
  70. package/dist-esm/types/ai/safety-classification.js.map +1 -0
  71. package/dist-esm/types/audit/ocsf.d.ts +32 -0
  72. package/dist-esm/types/audit/ocsf.d.ts.map +1 -1
  73. package/dist-esm/types/audit/ocsf.js +2 -0
  74. package/dist-esm/types/audit/ocsf.js.map +1 -1
  75. package/dist-esm/types/billing/billing.d.ts +1 -0
  76. package/dist-esm/types/billing/billing.d.ts.map +1 -1
  77. package/dist-esm/types/billing/billing.js +9 -0
  78. package/dist-esm/types/billing/billing.js.map +1 -1
  79. package/dist-esm/types/billing/index.d.ts +1 -0
  80. package/dist-esm/types/billing/index.d.ts.map +1 -1
  81. package/dist-esm/types/billing/index.js +1 -0
  82. package/dist-esm/types/billing/index.js.map +1 -1
  83. package/dist-esm/types/billing/spendAlert.d.ts +62 -0
  84. package/dist-esm/types/billing/spendAlert.d.ts.map +1 -0
  85. package/dist-esm/types/billing/spendAlert.js +10 -0
  86. package/dist-esm/types/billing/spendAlert.js.map +1 -0
  87. package/dist-esm/types/policyGate/index.d.ts +24 -0
  88. package/dist-esm/types/policyGate/index.d.ts.map +1 -1
  89. package/dist-esm/types/policyGate/index.js +8 -1
  90. package/dist-esm/types/policyGate/index.js.map +1 -1
  91. package/dist-esm/types/rbac/index.d.ts +4 -0
  92. package/dist-esm/types/rbac/index.d.ts.map +1 -1
  93. package/dist-esm/types/rbac/index.js +4 -0
  94. package/dist-esm/types/rbac/index.js.map +1 -1
  95. package/dist-esm/types/reviewPolicy/index.d.ts +9 -3
  96. package/dist-esm/types/reviewPolicy/index.d.ts.map +1 -1
  97. package/dist-esm/types/reviewPolicy/index.js +4 -0
  98. package/dist-esm/types/reviewPolicy/index.js.map +1 -1
  99. package/package.json +2 -2
  100. package/src/database-lifecycle/index.test.ts +6 -6
  101. package/src/database-lifecycle/index.ts +76 -152
  102. package/src/socket/protocol.ts +19 -2
  103. package/src/types/ai/index.ts +1 -0
  104. package/src/types/ai/quota-paywall.ts +20 -0
  105. package/src/types/ai/safety-classification.ts +27 -0
  106. package/src/types/audit/ocsf.ts +43 -0
  107. package/src/types/billing/billing.ts +11 -0
  108. package/src/types/billing/index.ts +1 -0
  109. package/src/types/billing/spendAlert.ts +84 -0
  110. package/src/types/policyGate/index.ts +28 -0
  111. package/src/types/rbac/index.ts +4 -0
  112. 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
- export const LIFECYCLE_OPERATIONS = ['ensure_dev_database', 'ensure_prod_database', 'migrate_schema', 'retire_database'] as const;
8
- export const ENVIRONMENT_CLASSES = ['dev', 'staging', 'prod'] as const;
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
- export type EnvironmentClass = (typeof ENVIRONMENT_CLASSES)[number];
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
- environmentClass: EnvironmentClass;
113
- environmentName: string;
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
- export type TerraformModuleInput = {
152
- bindingKey: string;
153
- requirement: DatabaseRequirement;
154
- credentialRefs: Record<string, CredentialRef>;
155
- metadata: Record<string, string>;
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
- // Resolved per-dispatch Terraform module reference. Mirrors the Go worker's
165
- // `pkg/databaselifecycle.TerraformModule` JSON shape — `MaterializeJob`
166
- // writes these straight into `terraform.tfvars.json` and the generated root
167
- // `main.tf` module block.
168
- export type TerraformModuleDispatch = {
169
- source: string;
170
- version: string;
171
- inputs: Record<string, unknown>;
172
- };
173
-
174
- // Backend config the worker writes into `backend.tfbackend` and passes to
175
- // `terraform init -backend-config=...`. Derived from the profile's
176
- // `backend.stateBackend` plus the per-binding state key. `stateBackend` is
177
- // the discriminant the worker uses to emit the right HCL
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
- // `tofu apply` succeeds. `version` is the sort key + the primary key in
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. Replaces three duplicate definitions
205
- // previously living in dispatchQueue.ts / devPersistence.ts / planner.
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 — `ensure_dev_database` / `ensure_prod_database` / `migrate_schema`
209
- // dispatches carry an array (possibly empty); `retire_database` omits it.
210
- // An omitted slice keeps the worker's `MigrationState` at the default
211
- // "pending"; an empty slice means "vacuous truth, mark migrated"; a
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
- profileId: string;
180
+ profile: string;
222
181
  requestId: string;
223
182
  resourceKey: string;
224
183
  runtimeCredentialRefs?: Record<string, CredentialRef>;
225
- // Present for Terraform-backed operations. Omitted for native migration
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
- environmentClass: EnvironmentClass;
240
- environmentName: string;
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
- environmentClass: EnvironmentClass;
249
- environmentName: string;
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
- environmentClass: EnvironmentClass;
258
- environmentName: string;
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 two different profiles could plausibly target the same logical
277
- // resource the planner uses profileId here so a profile swap forces a
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
- environmentClass: EnvironmentClass;
285
- environmentName: string;
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.environmentClass,
294
- `${slugify(input.environmentName)}~${encodeURIComponent(input.environmentName)}`,
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
  }
@@ -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
- { applicationId: string; branchName?: string; hash: string; source?: string },
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
  };
@@ -123,3 +123,4 @@ export interface AiPromptCostResponse {
123
123
 
124
124
  export * from './provider-credential.js';
125
125
  export * from './quota-paywall.js';
126
+ export * from './safety-classification.js';
@@ -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
+ }
@@ -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
  }
@@ -1,2 +1,3 @@
1
1
  export * from './freeCreditsTier.js';
2
2
  export * from './billing.js';
3
+ export * from './spendAlert.js';
@@ -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';