@treeseed/sdk 0.13.0-rc.102 → 0.13.0-rc.104

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.
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { AI_MODE_INTERNAL_PATH } from "./ai-mode.js";
3
- import { postgresComponentRequirementSchema } from "./postgres/contracts.js";
3
+ import { postgresComponentRequirementSchema, postgresTopologySchema } from "./postgres/contracts.js";
4
4
  const identifier = z.string().regex(/^[a-z][a-z0-9.-]{1,63}$/u);
5
5
  const digest = z.string().regex(/^sha256:[a-f0-9]{64}$/u);
6
6
  const gitCommit = z.string().regex(/^[a-f0-9]{40}$/u);
@@ -121,6 +121,7 @@ const hostConfigurationSchema = z.object({
121
121
  development: z.object({ pollSeconds: z.literal(60) }).strict()
122
122
  }).strict(),
123
123
  components: z.record(identifier, hostComponentSchema),
124
+ postgres: postgresTopologySchema.optional(),
124
125
  network: z.object({
125
126
  manager: z.object({ binding, aliases: z.array(localAlias).default([]), sans: z.array(z.string().min(1)).default([]), trustedLanCidrs: z.array(z.string().min(1)).default([]) }).strict()
126
127
  }).strict(),
@@ -149,7 +150,21 @@ const hostConfigurationSchema = z.object({
149
150
  providerVolume: z.object({ encryption: z.literal("luks2"), backingPath: z.string().startsWith("/"), mountPath: z.string().startsWith("/"), sizeBytes: z.number().int().min(1073741824), unlock: z.enum(["tpm2", "systemd-credential"]), recoveryRequired: z.literal(true) }).strict(),
150
151
  applicationEncryption: z.object({ provider: z.literal("systemd-credential"), activeKeyVersion: z.number().int().positive(), diagnosticsKeyVersion: z.number().int().positive() }).strict()
151
152
  }).strict().optional()
152
- }).strict();
153
+ }).strict().superRefine((host, context) => {
154
+ if (!host.postgres) return;
155
+ const fail = (message) => context.addIssue({ code: z.ZodIssueCode.custom, path: ["postgres"], message });
156
+ for (const requirement of host.postgres.requirements) {
157
+ if (!host.components[requirement.componentId] || host.components[requirement.componentId].enabled !== requirement.enabled) fail("Database requirements must match configured component activation.");
158
+ }
159
+ for (const allocation of host.postgres.allocations) {
160
+ if (!host.postgres.requirements.find((item) => item.id === allocation.requirementId)?.enabled) continue;
161
+ for (const reference of [allocation.migrationCredentialReference, allocation.runtimeCredentialReference]) {
162
+ if (host.secrets[reference]?.provider !== "systemd-credential" || host.secrets[reference]?.reference !== `/etc/treeseed/credentials/${reference}.cred`) fail("Database credentials require explicit fixed OS custody bindings.");
163
+ }
164
+ const server = host.postgres.servers.find((item) => item.id === allocation.serverId);
165
+ if (server.mode !== "external" && !host.components.postgres?.enabled) fail("Managed database allocations require the PostgreSQL component.");
166
+ }
167
+ });
153
168
  const packageEndpointSchema = z.object({
154
169
  id: identifier,
155
170
  protocol: z.enum(["http", "https", "tcp"]),
@@ -205,6 +220,12 @@ const packageRuntimeSchema = z.object({
205
220
  configuration: componentRuntimeConfigurationSchema,
206
221
  // Optional without a default: parsing published artifacts must not change their digest.
207
222
  postgresRequirements: z.array(postgresComponentRequirementSchema).max(32).optional(),
223
+ postgresLifecycle: z.array(z.object({
224
+ requirementId: identifier,
225
+ credentialOwner: z.object({ uid: z.number().int().min(0).max(65535), gid: z.number().int().min(0).max(65535) }).strict(),
226
+ migration: z.object({ composeService: identifier, completion: z.enum(["exit-zero", "healthy-stop"]), timeoutSeconds: z.number().int().min(1).max(3600) }).strict(),
227
+ runtimeServices: z.array(identifier).min(1).max(128)
228
+ }).strict()).max(32).optional(),
208
229
  services: z.array(z.object({ id: identifier, composeService: identifier, endpoints: z.array(packageEndpointSchema) }).strict()).min(1),
209
230
  stateVolumes: z.array(z.object({ id: identifier, volume: z.string().min(1), backup: z.enum(["required", "optional", "none"]) }).strict()),
210
231
  migrations: z.array(z.object({ id: identifier, order: z.number().int().nonnegative(), backupRequired: z.boolean() }).strict()),
@@ -236,6 +257,15 @@ const packageRuntimeSchema = z.object({
236
257
  }).strict().superRefine((runtime, context) => {
237
258
  const requirements = runtime.postgresRequirements ?? [];
238
259
  if (new Set(requirements.map(({ id }) => id)).size !== requirements.length) context.addIssue({ code: z.ZodIssueCode.custom, path: ["postgresRequirements"], message: "PostgreSQL requirement identities must be unique within a component." });
260
+ const lifecycles = runtime.postgresLifecycle ?? [];
261
+ const lifecycleFailure = () => context.addIssue({ code: z.ZodIssueCode.custom, path: ["postgresLifecycle"], message: "Each database requirement needs a distinct declared migration service and explicit runtime services." });
262
+ if (lifecycles.length !== requirements.length || new Set(lifecycles.map((item) => item.requirementId)).size !== lifecycles.length) lifecycleFailure();
263
+ const composeServices = new Set(runtime.services.map((item) => item.composeService));
264
+ const migrationServices = new Set(lifecycles.map((item) => item.migration.composeService));
265
+ if (migrationServices.size !== lifecycles.length) lifecycleFailure();
266
+ for (const item of lifecycles) {
267
+ if (!requirements.some((requirement) => requirement.id === item.requirementId) || !composeServices.has(item.migration.composeService) || new Set(item.runtimeServices).size !== item.runtimeServices.length || item.runtimeServices.some((service) => !composeServices.has(service) || migrationServices.has(service))) lifecycleFailure();
268
+ }
239
269
  const endpointIds = runtime.services.flatMap((service) => service.endpoints.map((endpoint) => `${service.id}.${endpoint.id}`));
240
270
  if (new Set(endpointIds).size !== endpointIds.length) context.addIssue({ code: z.ZodIssueCode.custom, path: ["services"], message: "Endpoint identities must be unique within a component." });
241
271
  for (const file of runtime.configuration.files) if (!file.path.startsWith(`/etc/treeseed/components/${runtime.componentId}/`)) context.addIssue({ code: z.ZodIssueCode.custom, path: ["configuration", "files"], message: `Configuration file ${file.id} is outside component ${runtime.componentId} custody.` });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.13.0-rc.102",
3
+ "version": "0.13.0-rc.104",
4
4
  "description": "Portable TreeSeed contracts, standards, and typed remote control-plane clients.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {