@intentius/chant-lexicon-render 0.124.30 → 0.124.32

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 (49) hide show
  1. package/dist/catalog.js +221 -0
  2. package/dist/codegen/docs-cli.js +6 -0
  3. package/dist/codegen/docs.js +110 -0
  4. package/dist/codegen/generate-cli.js +26 -0
  5. package/dist/codegen/generate-lexicon.js +38 -0
  6. package/dist/codegen/generate-typescript.js +46 -0
  7. package/dist/codegen/generate.js +82 -0
  8. package/dist/codegen/naming.js +43 -0
  9. package/dist/codegen/package.js +27 -0
  10. package/dist/composites/render-deploy.js +71 -0
  11. package/dist/coverage.js +29 -0
  12. package/dist/describe-resources.js +325 -0
  13. package/dist/detect.js +27 -0
  14. package/dist/generated/index.js +40 -0
  15. package/dist/generated/runtime.js +4 -0
  16. package/dist/index.js +21 -0
  17. package/dist/integrity.json +2 -2
  18. package/dist/lint/audit-catalog.js +23 -0
  19. package/dist/lint/audit-lineage.js +8 -0
  20. package/dist/lint/post-synth/index.js +8 -0
  21. package/dist/lint/post-synth/ren010-native-runtime-requires-commands.js +43 -0
  22. package/dist/lint/post-synth/ren011-service-requires-source.js +45 -0
  23. package/dist/lint/post-synth/ren012-free-plan-cannot-scale.js +41 -0
  24. package/dist/lint/post-synth/render-helpers.js +34 -0
  25. package/dist/lint/rules/index.js +8 -0
  26. package/dist/lint/rules/no-secret-literals.js +68 -0
  27. package/dist/lint/rules/valid-cron-schedule.js +44 -0
  28. package/dist/lint/rules/valid-region.js +43 -0
  29. package/dist/lsp/completions.d.ts.map +1 -1
  30. package/dist/lsp/completions.js +20 -0
  31. package/dist/lsp/hover.d.ts.map +1 -1
  32. package/dist/lsp/hover.js +49 -0
  33. package/dist/manifest.json +1 -1
  34. package/dist/op/activities/fake-render.js +271 -0
  35. package/dist/op/activities/index.js +8 -0
  36. package/dist/op/activities/render-apply.js +873 -0
  37. package/dist/ownership.js +38 -0
  38. package/dist/package-cli.js +17 -0
  39. package/dist/plugin.js +229 -0
  40. package/dist/pseudo.js +23 -0
  41. package/dist/serializer.js +238 -0
  42. package/dist/spec/fetch.js +30 -0
  43. package/dist/spec/parse.js +600 -0
  44. package/dist/validate-cli.js +4 -0
  45. package/dist/validate.js +41 -0
  46. package/package.json +7 -7
  47. package/src/codegen/package.ts +2 -2
  48. package/src/lsp/completions.ts +2 -1
  49. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,41 @@
1
+ /**
2
+ * REN012: The free instance type cannot scale.
3
+ *
4
+ * Render's `free` plan runs a single instance and cannot autoscale; a
5
+ * `numInstances > 1` or an enabled `autoscaling` block on a free service is
6
+ * rejected at create/patch time. Persistent disks are also unavailable on
7
+ * free. Catch all three at synth time with a message that says which.
8
+ */
9
+ import { readProps, isService, kindOf } from "./render-helpers.js";
10
+ export const ren012 = {
11
+ id: "REN012",
12
+ description: "A free-plan service cannot set numInstances > 1, autoscaling, or a disk",
13
+ check(ctx) {
14
+ const diagnostics = [];
15
+ for (const [name, entity] of ctx.entities) {
16
+ if (!isService(entity))
17
+ continue;
18
+ const details = readProps(readProps(entity).serviceDetails);
19
+ if (details.plan !== "free")
20
+ continue;
21
+ const problems = [];
22
+ if (typeof details.numInstances === "number" && details.numInstances > 1)
23
+ problems.push(`numInstances: ${details.numInstances}`);
24
+ const autoscaling = readProps(details.autoscaling);
25
+ if (autoscaling.enabled === true)
26
+ problems.push("autoscaling");
27
+ if (details.disk !== undefined)
28
+ problems.push("a disk");
29
+ if (problems.length > 0) {
30
+ diagnostics.push({
31
+ checkId: "REN012",
32
+ severity: "error",
33
+ message: `${kindOf(entity)} "${name}" is on the free plan but sets ${problems.join(", ")} — the free instance type runs one instance, cannot autoscale, and has no persistent disk; pick a paid plan (starter or above)`,
34
+ entity: name,
35
+ lexicon: "render",
36
+ });
37
+ }
38
+ }
39
+ return diagnostics;
40
+ },
41
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared helpers for the render post-synth checks. Not a check itself, so the
3
+ * generated barrel skips it (the scanner only picks up exported PostSynthChecks).
4
+ */
5
+ import { isServiceEntityType } from "../../catalog.js";
6
+ /**
7
+ * Read an entity or property's constructor props. Nested Declarables
8
+ * (WebServiceDetails, Image, ServiceDisk) stash their args under a
9
+ * non-enumerable `props`; a plain inline object carries them directly. Handle
10
+ * both so a check behaves the same whether the user authored
11
+ * `serviceDetails: { runtime }` or `serviceDetails: new WebServiceDetails({ runtime })`.
12
+ */
13
+ export function readProps(value) {
14
+ if (value && typeof value === "object") {
15
+ const nested = value.props;
16
+ if (nested && typeof nested === "object")
17
+ return nested;
18
+ return value;
19
+ }
20
+ return {};
21
+ }
22
+ /** The entityType of a declarable, or undefined. */
23
+ export function entityTypeOf(value) {
24
+ return value?.entityType;
25
+ }
26
+ /** True when the entity is one of the five service kinds. */
27
+ export function isService(value) {
28
+ return isServiceEntityType(entityTypeOf(value));
29
+ }
30
+ /** The short kind (`WebService`) from an entity type, for messages. */
31
+ export function kindOf(value) {
32
+ const t = entityTypeOf(value) ?? "";
33
+ return t.slice(t.lastIndexOf("::") + 2);
34
+ }
@@ -0,0 +1,8 @@
1
+ import { validRegionRule } from "./valid-region.js";
2
+ import { noSecretLiteralsRule } from "./no-secret-literals.js";
3
+ import { validCronScheduleRule } from "./valid-cron-schedule.js";
4
+ export { validRegionRule } from "./valid-region.js";
5
+ export { noSecretLiteralsRule } from "./no-secret-literals.js";
6
+ export { validCronScheduleRule, isValidCronSchedule } from "./valid-cron-schedule.js";
7
+ /** All lint rules provided by the render lexicon. */
8
+ export const rules = [validRegionRule, noSecretLiteralsRule, validCronScheduleRule];
@@ -0,0 +1,68 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /**
3
+ * Env-var keys that carry a credential. Kept conservative on purpose — an
4
+ * inline value under one of these keys is very likely a real secret.
5
+ */
6
+ const SECRET_KEY_PATTERN = /(?:^|[_-])(?:password|passwd|secret|token|api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|credential|auth[_-]?token)(?:$|[_-])/i;
7
+ /**
8
+ * Values that are references or placeholders, not literal secrets: shell/
9
+ * interpolation references ($FOO, ${FOO}) and secret-manager style references.
10
+ */
11
+ const REFERENCE_VALUE_PATTERN = /^(?:\$\{?[A-Za-z0-9_]+\}?|(?:secret|ref|env|vault):\S+)$/;
12
+ /**
13
+ * REN002: No secret literals in env vars
14
+ *
15
+ * Flags an `envVars` (or `secretFiles`) entry whose `key` looks like a
16
+ * credential and whose `value` is a plain string literal. Render can generate
17
+ * the value (`generateValue: true`), it can come from an EnvGroup or from
18
+ * another resource's attribute (`db.internalConnectionString`), or from the
19
+ * process environment at build time — anything but a literal in source that
20
+ * ends up in git and in the build output.
21
+ */
22
+ export const noSecretLiteralsRule = {
23
+ id: "REN002",
24
+ severity: "warning",
25
+ category: "security",
26
+ description: "Secret env-var values must not be written inline — use generateValue, an EnvGroup, or a reference",
27
+ check(context) {
28
+ const { sourceFile } = context;
29
+ const diagnostics = [];
30
+ function literalOf(init) {
31
+ return ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init) ? init.text : undefined;
32
+ }
33
+ function visit(node) {
34
+ // `{ key: "DB_PASSWORD", value: "hunter2" }` — an object literal with a
35
+ // secret-looking `key` and a literal `value`.
36
+ if (ts.isObjectLiteralExpression(node)) {
37
+ let key;
38
+ let valueNode;
39
+ for (const prop of node.properties) {
40
+ if (!ts.isPropertyAssignment(prop))
41
+ continue;
42
+ const name = prop.name.getText(sourceFile).replace(/^["']|["']$/g, "");
43
+ if (name === "key")
44
+ key = literalOf(prop.initializer);
45
+ if (name === "value")
46
+ valueNode = prop.initializer;
47
+ }
48
+ if (key && valueNode) {
49
+ const value = literalOf(valueNode);
50
+ if (value !== undefined && value.length >= 4 && SECRET_KEY_PATTERN.test(key) && !REFERENCE_VALUE_PATTERN.test(value.trim())) {
51
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(valueNode.getStart(sourceFile));
52
+ diagnostics.push({
53
+ file: sourceFile.fileName,
54
+ line: line + 1,
55
+ column: character + 1,
56
+ ruleId: "REN002",
57
+ severity: "warning",
58
+ message: `Possible inline secret for env var "${key}". Use \`generateValue: true\`, an EnvGroup, a resource attribute (e.g. db.internalConnectionString), or process.env — not a literal.`,
59
+ });
60
+ }
61
+ }
62
+ }
63
+ ts.forEachChild(node, visit);
64
+ }
65
+ visit(sourceFile);
66
+ return diagnostics;
67
+ },
68
+ };
@@ -0,0 +1,44 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /** One cron field: `*`, a number, a range, a list, or any of those with a `/step`. */
3
+ const FIELD = String.raw `(?:\*|\d+(?:-\d+)?)(?:,(?:\*|\d+(?:-\d+)?))*(?:\/\d+)?`;
4
+ const CRON_5 = new RegExp(`^${FIELD}(?:\\s+${FIELD}){4}$`);
5
+ /** True for a standard five-field cron expression (Render accepts no seconds field or macros). */
6
+ export function isValidCronSchedule(s) {
7
+ return CRON_5.test(s.trim());
8
+ }
9
+ /**
10
+ * REN003: Valid cron schedule
11
+ *
12
+ * A CronJob's `schedule` must be a standard five-field cron expression
13
+ * (`minute hour day-of-month month day-of-week`). Render rejects `@hourly`-style
14
+ * macros and six-field (seconds) expressions at create time; catch them at build.
15
+ */
16
+ export const validCronScheduleRule = {
17
+ id: "REN003",
18
+ severity: "error",
19
+ category: "correctness",
20
+ description: "A cron job schedule must be a five-field cron expression",
21
+ check(context) {
22
+ const { sourceFile } = context;
23
+ const diagnostics = [];
24
+ function visit(node) {
25
+ if (ts.isPropertyAssignment(node) && node.name.getText(sourceFile) === "schedule") {
26
+ const init = node.initializer;
27
+ if (ts.isStringLiteral(init) && !isValidCronSchedule(init.text)) {
28
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(init.getStart(sourceFile));
29
+ diagnostics.push({
30
+ file: sourceFile.fileName,
31
+ line: line + 1,
32
+ column: character + 1,
33
+ ruleId: "REN003",
34
+ severity: "error",
35
+ message: `Invalid cron schedule "${init.text}". Use five fields, e.g. "0 * * * *" (hourly) or "*/15 * * * *" (every 15 minutes); Render accepts no @macros or seconds field.`,
36
+ });
37
+ }
38
+ }
39
+ ts.forEachChild(node, visit);
40
+ }
41
+ visit(sourceFile);
42
+ return diagnostics;
43
+ },
44
+ };
@@ -0,0 +1,43 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /**
3
+ * Render's regions, per the `region` enum in the Public API spec. The generated
4
+ * types already narrow `region` to this union, so a typo is a compile error in
5
+ * a typed project; this rule gives the same answer with a clearer message, and
6
+ * fires in contexts the type checker does not reach (a `.js` stack, a value
7
+ * that has been widened to `string`).
8
+ */
9
+ export const KNOWN_REGIONS = new Set(["frankfurt", "oregon", "ohio", "singapore", "virginia"]);
10
+ /**
11
+ * REN001: Valid Render region
12
+ *
13
+ * A service, datastore, or Key Value `region` must be a Render region.
14
+ */
15
+ export const validRegionRule = {
16
+ id: "REN001",
17
+ severity: "error",
18
+ category: "correctness",
19
+ description: "A region must be a Render region (frankfurt, oregon, ohio, singapore, virginia)",
20
+ check(context) {
21
+ const { sourceFile } = context;
22
+ const diagnostics = [];
23
+ function visit(node) {
24
+ if (ts.isPropertyAssignment(node) && node.name.getText(sourceFile) === "region") {
25
+ const init = node.initializer;
26
+ if (ts.isStringLiteral(init) && init.text.length > 0 && !KNOWN_REGIONS.has(init.text)) {
27
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(init.getStart(sourceFile));
28
+ diagnostics.push({
29
+ file: sourceFile.fileName,
30
+ line: line + 1,
31
+ column: character + 1,
32
+ ruleId: "REN001",
33
+ severity: "error",
34
+ message: `Unknown Render region "${init.text}". Use one of: ${[...KNOWN_REGIONS].join(", ")}.`,
35
+ });
36
+ }
37
+ }
38
+ ts.forEachChild(node, visit);
39
+ }
40
+ visit(sourceFile);
41
+ return diagnostics;
42
+ },
43
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAapF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAEpE"}
1
+ {"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAapF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAEpE"}
@@ -0,0 +1,20 @@
1
+ import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
3
+ import { LexiconIndex, lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
4
+ const require = createRequire(moduleSourceUrl(import.meta.url));
5
+ let cachedIndex = null;
6
+ function getIndex() {
7
+ if (cachedIndex)
8
+ return cachedIndex;
9
+ const data = require("../generated/lexicon-render.json");
10
+ cachedIndex = new LexiconIndex(data);
11
+ return cachedIndex;
12
+ }
13
+ /**
14
+ * Provide render completions based on context — the resource classes
15
+ * (WebService, Postgres, EnvGroup, ...) and their property entities, from the
16
+ * generated registry.
17
+ */
18
+ export function completions(ctx) {
19
+ return lexiconCompletions(ctx, getIndex(), "Render resource");
20
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAc1E;;GAEG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAE9D"}
1
+ {"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAc1E;;GAEG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAE9D"}
@@ -0,0 +1,49 @@
1
+ import { createRequire } from "module";
2
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
3
+ import { LexiconIndex, lexiconHover } from "@intentius/chant/lsp/lexicon-providers";
4
+ import { CATALOG } from "../catalog.js";
5
+ const require = createRequire(moduleSourceUrl(import.meta.url));
6
+ let cachedIndex = null;
7
+ function getIndex() {
8
+ if (cachedIndex)
9
+ return cachedIndex;
10
+ const data = require("../generated/lexicon-render.json");
11
+ cachedIndex = new LexiconIndex(data);
12
+ return cachedIndex;
13
+ }
14
+ /**
15
+ * Provide hover information for render entity types.
16
+ */
17
+ export function hover(ctx) {
18
+ return lexiconHover(ctx, getIndex(), resourceHover);
19
+ }
20
+ function resourceHover(className, entry) {
21
+ const lines = [];
22
+ lines.push(`**${className}**`);
23
+ lines.push("");
24
+ lines.push(`Render type: \`${entry.resourceType}\``);
25
+ lines.push("");
26
+ const cat = CATALOG[entry.resourceType];
27
+ if (entry.kind === "resource" && cat) {
28
+ lines.push(`*Resource — created with \`POST ${cat.collection}\` on the Render Public API and reconciled by name*`);
29
+ if (cat.marked) {
30
+ lines.push("");
31
+ lines.push("Carries chant's `CHANT_MANAGED_BY` env-var ownership marker; eligible for owned-only prune.");
32
+ }
33
+ else if (cat.boundary === "service") {
34
+ lines.push("");
35
+ lines.push("Inherits its parent service's ownership verdict (service boundary); pruned when the service is chant-owned and this is undeclared.");
36
+ }
37
+ else {
38
+ lines.push("");
39
+ lines.push("No ownership marker channel: verdict is `unknown`; never auto-pruned.");
40
+ }
41
+ }
42
+ else if (entry.kind === "resource") {
43
+ lines.push("*Resource — a Render Public API object*");
44
+ }
45
+ else {
46
+ lines.push("*Property — a nested value in a Render resource declaration*");
47
+ }
48
+ return { contents: lines.join("\n") };
49
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "render",
3
- "version": "0.124.30",
3
+ "version": "0.124.32",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Render"
6
6
  }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * An in-memory stand-in for the Render Public API, for tests. Implements the
3
+ * slice of routes the applier and the observation seam use — list/create/
4
+ * patch/delete for every collection in the catalog, service env vars, env
5
+ * group vars and links, deploys, connection-info, owners — with Render's list
6
+ * envelope (`[{ service: {...}, cursor }]`) and id prefixes. Not an emulator of
7
+ * Render's semantics beyond what chant relies on; it exists so the reconcile
8
+ * loop is tested end to end without an account.
9
+ */
10
+ export class FakeRender {
11
+ constructor() {
12
+ this.owners = [{ id: "tea-1", name: "Acme", email: "ops@acme.test", type: "team" }];
13
+ this.collections = {
14
+ "/services": { key: "service", prefix: "srv", items: new Map() },
15
+ "/postgres": { key: "postgres", prefix: "dpg", items: new Map() },
16
+ "/key-value": { key: "keyValue", prefix: "red", items: new Map() },
17
+ "/env-groups": { key: "envGroup", prefix: "evg", items: new Map() },
18
+ "/projects": { key: "project", prefix: "prj", items: new Map() },
19
+ "/environments": { key: "environment", prefix: "evm", items: new Map() },
20
+ "/disks": { key: "disk", prefix: "dsk", items: new Map() },
21
+ "/registrycredentials": { key: "registryCredential", prefix: "rgc", items: new Map() },
22
+ "/webhooks": { key: "webhook", prefix: "whk", items: new Map() },
23
+ };
24
+ /** serviceId → env vars */
25
+ this.serviceEnv = new Map();
26
+ /** serviceId → deploys (newest first) */
27
+ this.deploys = new Map();
28
+ /** serviceId → custom domains */
29
+ this.domains = new Map();
30
+ /** Every call, for assertions. */
31
+ this.calls = [];
32
+ /** Deploys go live after this many polls (0 = immediately). */
33
+ this.deployPolls = 0;
34
+ this.counter = 0;
35
+ }
36
+ id(prefix) {
37
+ this.counter++;
38
+ return `${prefix}-${String(this.counter).padStart(20, "0")}`;
39
+ }
40
+ /** Seed a resource directly. */
41
+ seed(collection, rec) {
42
+ const c = this.collections[collection];
43
+ const id = typeof rec.id === "string" ? rec.id : this.id(c.prefix);
44
+ const full = { id, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", ...rec };
45
+ c.items.set(id, full);
46
+ if (collection === "/services") {
47
+ this.serviceEnv.set(id, [...(rec.envVars ?? [])]);
48
+ this.deploys.set(id, [{ id: this.id("dep"), status: "live" }]);
49
+ }
50
+ return full;
51
+ }
52
+ service(name) {
53
+ return [...this.collections["/services"].items.values()].find((s) => s.name === name);
54
+ }
55
+ http() {
56
+ return async (method, url, body) => {
57
+ const u = new URL(url);
58
+ const path = u.pathname.replace(/^\/v1/, "");
59
+ this.calls.push({ method, path, body });
60
+ return this.route(method, path, u.searchParams, body);
61
+ };
62
+ }
63
+ json(status, value) {
64
+ return { status, text: value === undefined ? "" : JSON.stringify(value) };
65
+ }
66
+ route(method, path, q, body) {
67
+ const b = (body ?? {});
68
+ if (path === "/owners" && method === "GET") {
69
+ return this.json(200, this.owners.map((o) => ({ owner: o, cursor: String(o.id) })));
70
+ }
71
+ // Service sub-resources.
72
+ let m = path.match(/^\/services\/([^/]+)\/env-vars$/);
73
+ if (m) {
74
+ const id = m[1];
75
+ if (!this.collections["/services"].items.has(id))
76
+ return this.json(404, { message: "not found" });
77
+ if (method === "GET") {
78
+ return this.json(200, (this.serviceEnv.get(id) ?? []).map((e) => ({ envVar: e, cursor: e.key })));
79
+ }
80
+ if (method === "PUT") {
81
+ const list = body.map((e) => ({
82
+ key: e.key,
83
+ value: e.value ?? (e.generateValue ? `gen-${e.key}` : ""),
84
+ }));
85
+ this.serviceEnv.set(id, list);
86
+ return this.json(200, list.map((e) => ({ envVar: e, cursor: e.key })));
87
+ }
88
+ }
89
+ m = path.match(/^\/services\/([^/]+)\/deploys$/);
90
+ if (m && method === "GET") {
91
+ const list = this.deploys.get(m[1]) ?? [];
92
+ return this.json(200, list.map((d) => ({ deploy: this.tickDeploy(d), cursor: d.id })));
93
+ }
94
+ m = path.match(/^\/services\/([^/]+)\/deploys\/([^/]+)$/);
95
+ if (m && method === "GET") {
96
+ const d = (this.deploys.get(m[1]) ?? []).find((x) => x.id === m[2]);
97
+ if (!d)
98
+ return this.json(404, { message: "not found" });
99
+ return this.json(200, this.tickDeploy(d));
100
+ }
101
+ m = path.match(/^\/services\/([^/]+)\/custom-domains$/);
102
+ if (m) {
103
+ const id = m[1];
104
+ if (!this.collections["/services"].items.has(id))
105
+ return this.json(404, { message: "not found" });
106
+ const list = this.domains.get(id) ?? [];
107
+ if (method === "GET")
108
+ return this.json(200, list.map((d) => ({ customDomain: d, cursor: d.id })));
109
+ if (method === "POST") {
110
+ const d = { id: this.id("cd"), name: b.name, domainType: "apex", verificationStatus: "unverified", createdAt: "2026-01-01T00:00:00Z" };
111
+ list.push(d);
112
+ this.domains.set(id, list);
113
+ return this.json(201, [d]);
114
+ }
115
+ }
116
+ m = path.match(/^\/services\/([^/]+)\/custom-domains\/([^/]+)$/);
117
+ if (m && method === "DELETE") {
118
+ const list = this.domains.get(m[1]) ?? [];
119
+ const idx = list.findIndex((d) => d.id === m[2] || d.name === m[2]);
120
+ if (idx < 0)
121
+ return this.json(404, {});
122
+ list.splice(idx, 1);
123
+ return this.json(204, undefined);
124
+ }
125
+ // Datastore connection info.
126
+ m = path.match(/^\/(postgres|key-value)\/([^/]+)\/connection-info$/);
127
+ if (m && method === "GET") {
128
+ const c = this.collections[`/${m[1]}`];
129
+ const rec = c.items.get(m[2]);
130
+ if (!rec)
131
+ return this.json(404, {});
132
+ return this.json(200, {
133
+ internalConnectionString: `internal://${rec.name}`,
134
+ externalConnectionString: `external://${rec.name}`,
135
+ psqlCommand: `psql ${rec.name}`,
136
+ cliCommand: `redis-cli ${rec.name}`,
137
+ });
138
+ }
139
+ // Env group vars and links.
140
+ m = path.match(/^\/env-groups\/([^/]+)\/env-vars\/([^/]+)$/);
141
+ if (m) {
142
+ const grp = this.collections["/env-groups"].items.get(m[1]);
143
+ if (!grp)
144
+ return this.json(404, {});
145
+ const vars = grp.envVars ?? [];
146
+ const key = decodeURIComponent(m[2]);
147
+ if (method === "PUT") {
148
+ const value = typeof b.value === "string" ? b.value : `gen-${key}`;
149
+ const idx = vars.findIndex((v) => v.key === key);
150
+ if (idx >= 0)
151
+ vars[idx] = { key, value };
152
+ else
153
+ vars.push({ key, value });
154
+ grp.envVars = vars;
155
+ return this.json(200, { key, value });
156
+ }
157
+ if (method === "DELETE") {
158
+ grp.envVars = vars.filter((v) => v.key !== key);
159
+ return this.json(204, undefined);
160
+ }
161
+ }
162
+ m = path.match(/^\/env-groups\/([^/]+)\/services\/([^/]+)$/);
163
+ if (m && method === "POST") {
164
+ const grp = this.collections["/env-groups"].items.get(m[1]);
165
+ if (!grp)
166
+ return this.json(404, {});
167
+ const links = grp.serviceLinks ?? [];
168
+ if (!links.some((l) => l.id === m[2]))
169
+ links.push({ id: m[2], name: m[2], type: "web_service" });
170
+ grp.serviceLinks = links;
171
+ return this.json(200, grp);
172
+ }
173
+ // Collections.
174
+ for (const [collection, c] of Object.entries(this.collections)) {
175
+ if (path === collection) {
176
+ if (method === "GET") {
177
+ let items = [...c.items.values()];
178
+ const name = q.get("name");
179
+ const ownerId = q.get("ownerId");
180
+ const projectId = q.get("projectId");
181
+ const serviceId = q.get("serviceId");
182
+ if (name)
183
+ items = items.filter((i) => i.name === name);
184
+ // Disks/domains carry no ownerId field on the wire; Render scopes them
185
+ // server-side. The fake has one owner, so only filter what has one.
186
+ if (ownerId)
187
+ items = items.filter((i) => (i.ownerId === undefined && i.owner === undefined) || i.ownerId === ownerId || i.owner?.id === ownerId);
188
+ if (projectId)
189
+ items = items.filter((i) => i.projectId === projectId);
190
+ if (serviceId)
191
+ items = items.filter((i) => i.serviceId === serviceId);
192
+ return this.json(200, items.map((i) => ({ [c.key]: i, cursor: String(i.id) })));
193
+ }
194
+ if (method === "POST") {
195
+ const id = this.id(c.prefix);
196
+ const rec = { ...b, id, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z" };
197
+ if (collection === "/services") {
198
+ const env = (b.envVars ?? []).map((e) => ({
199
+ key: e.key,
200
+ value: e.value ?? `gen-${e.key}`,
201
+ }));
202
+ delete rec.envVars;
203
+ delete rec.secretFiles;
204
+ rec.dashboardUrl = `https://dashboard.render.com/web/${id}`;
205
+ rec.slug = String(b.name);
206
+ rec.suspended = "not_suspended";
207
+ this.serviceEnv.set(id, env);
208
+ const dep = { id: this.id("dep"), status: this.deployPolls > 0 ? "build_in_progress" : "live" };
209
+ this.deploys.set(id, [dep]);
210
+ c.items.set(id, rec);
211
+ return this.json(201, { service: rec, deployId: dep.id });
212
+ }
213
+ if (collection === "/env-groups") {
214
+ rec.envVars = (b.envVars ?? []).map((e) => ({
215
+ key: e.key,
216
+ value: e.value ?? `gen-${e.key}`,
217
+ }));
218
+ rec.serviceLinks = (b.serviceIds ?? []).map((sid) => ({ id: sid, name: sid, type: "web_service" }));
219
+ delete rec.serviceIds;
220
+ }
221
+ if (collection === "/projects") {
222
+ rec.owner = { id: b.ownerId, name: "Acme", email: "", type: "team" };
223
+ rec.environmentIds = [];
224
+ }
225
+ if (collection === "/postgres" || collection === "/key-value") {
226
+ rec.owner = { id: b.ownerId, name: "Acme", email: "", type: "team" };
227
+ rec.status = "available";
228
+ rec.dashboardUrl = `https://dashboard.render.com/d/${id}`;
229
+ }
230
+ c.items.set(id, rec);
231
+ return this.json(201, rec);
232
+ }
233
+ }
234
+ const one = path.match(new RegExp(`^${collection.replace(/[-/]/g, "\\$&")}/([^/]+)$`));
235
+ if (one) {
236
+ const id = one[1];
237
+ const rec = c.items.get(id);
238
+ if (!rec)
239
+ return this.json(404, { message: "not found" });
240
+ if (method === "GET")
241
+ return this.json(200, rec);
242
+ if (method === "PATCH") {
243
+ const updated = { ...rec, ...b, updatedAt: "2026-01-02T00:00:00Z" };
244
+ if (rec.serviceDetails && b.serviceDetails) {
245
+ updated.serviceDetails = { ...rec.serviceDetails, ...b.serviceDetails };
246
+ }
247
+ c.items.set(id, updated);
248
+ if (collection === "/services") {
249
+ const deps = this.deploys.get(id) ?? [];
250
+ deps.unshift({ id: this.id("dep"), status: "live" });
251
+ }
252
+ return this.json(200, updated);
253
+ }
254
+ if (method === "DELETE") {
255
+ c.items.delete(id);
256
+ return this.json(204, undefined);
257
+ }
258
+ }
259
+ }
260
+ return this.json(404, { message: `fake-render: no route for ${method} ${path}` });
261
+ }
262
+ /** Advance a pending deploy toward `live` on each poll. */
263
+ tickDeploy(d) {
264
+ if (d.status !== "live" && this.deployPolls > 0) {
265
+ this.deployPolls--;
266
+ if (this.deployPolls === 0)
267
+ d.status = "live";
268
+ }
269
+ return { ...d };
270
+ }
271
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * render Op activities — resolved by the core activity registry when a
3
+ * project's `chant.config.ts` lists the `render` lexicon. Contributes the
4
+ * native Render API applier (`renderApply`), which speaks the Render Public
5
+ * API directly (no CLI, no Blueprint, no state file), and its teardown twin
6
+ * (`renderDelete`).
7
+ */
8
+ export { renderApply, renderApplyDetailed, renderDelete, toApplyResult, resolveEndpoint, parsePlan, orderPlan, collectDependencies, configEqual, diffForPatch, envVarsToMap, declaredEnvVarsToMap, envVarsDiffer, isChantOwned, ownershipOf, planOwnership, inStack, defaultRenderHttp, listAll, getOne, resolveOwner, findExisting, resolveAttribute, resolveMarkers, create, patch, remove, readServiceEnvVars, putServiceEnvVars, reconcileEnvGroupVars, linkEnvGroupServices, latestDeploy, waitForDeploy, DEFAULT_RENDER_BASE_URL, } from "./render-apply.js";