@mesh-tech/mesh-cli 0.38.1 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js → mesh.auth-preflight-JO5G7SOF.js} +1 -1
- package/dist/bin/{mesh.chunk-LMYPTJ2F.js → mesh.chunk-2ZJR5LGN.js} +573 -209
- package/dist/bin/mesh.chunk-2ZJR5LGN.js.map +6 -0
- package/dist/bin/{mesh.chunk-BF2SH6XT.js → mesh.chunk-C2TKCYHP.js} +1 -1
- package/dist/bin/{mesh.chunk-HPYZWPRM.js → mesh.chunk-C4ZM7DM2.js} +1 -1
- package/dist/bin/{mesh.chunk-I6NXLXGC.js → mesh.chunk-CNLSVKYY.js} +17 -15
- package/dist/bin/mesh.chunk-CNLSVKYY.js.map +6 -0
- package/dist/bin/{mesh.chunk-TZAGJTKU.js → mesh.chunk-O6WRZU4X.js} +1 -1
- package/dist/bin/{mesh.chunk-KN4BZOYH.js → mesh.chunk-OSV3EZQ3.js} +1 -1
- package/dist/bin/{mesh.chunk-2YRWJH5X.js → mesh.chunk-UI6EFAMD.js} +4 -4
- package/dist/bin/{mesh.chunk-2YRWJH5X.js.map → mesh.chunk-UI6EFAMD.js.map} +1 -1
- package/dist/bin/{mesh.chunk-IU2IWH6O.js → mesh.chunk-WODQO6TP.js} +3 -3
- package/dist/bin/{mesh.create-app-NGXACFAU.js → mesh.create-app-SOHBL75U.js} +3 -3
- package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js → mesh.dev-token-server-SS7QEQOQ.js} +2 -2
- package/dist/bin/mesh.js +8 -8
- package/dist/bin/{mesh.login-7SIAKFXJ.js → mesh.login-6VPMWHHU.js} +2 -2
- package/dist/bin/{mesh.platform-setup-CJXE3BCP.js → mesh.platform-setup-DJJB2PWL.js} +1 -1
- package/dist/bin/{mesh.program-BIDQBSSI.js → mesh.program-Y4HUNBFP.js} +8 -8
- package/dist/bin/{mesh.registry-7P4MGXEK.js → mesh.registry-5P4ZLFLJ.js} +4 -4
- package/dist/bin/{mesh.registry-identity-JW4KO7N7.js → mesh.registry-identity-ONGUJQAY.js} +3 -3
- package/dist/bin/{mesh.start-MPQFIEMH.js → mesh.start-5CPWOHDJ.js} +2 -2
- package/dist/bin/{mesh.wizard-ZJE4S6PC.js → mesh.wizard-FNRTSGVN.js} +7 -7
- package/dist/build-info.json +2 -2
- package/dist/src/commands/app-check.js +2 -2
- package/dist/src/commands/dev.js +35 -0
- package/dist/src/commands/local/app-authz.d.ts +118 -0
- package/dist/src/commands/local/app-authz.js +295 -0
- package/dist/src/commands/local/auth-provision.d.ts +1 -0
- package/dist/src/commands/local/auth-provision.js +12 -1
- package/dist/src/commands/local/index.js +2 -0
- package/dist/src/commands/temporal.js +16 -1
- package/dist/src/commands/temporal.js.map +2 -2
- package/fragments/base/index.ts.hbs +1 -1
- package/package.json +2 -2
- package/templates/api-auth/api/src/schema.ts.hbs +17 -0
- package/templates/api-auth/package.json.hbs +3 -0
- package/templates/api-role-gating/api/src/schema.ts.hbs +10 -0
- package/dist/bin/mesh.chunk-I6NXLXGC.js.map +0 -6
- package/dist/bin/mesh.chunk-LMYPTJ2F.js.map +0 -6
- /package/dist/bin/{mesh.auth-preflight-AQJ5TQES.js.map → mesh.auth-preflight-JO5G7SOF.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-BF2SH6XT.js.map → mesh.chunk-C2TKCYHP.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-HPYZWPRM.js.map → mesh.chunk-C4ZM7DM2.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-TZAGJTKU.js.map → mesh.chunk-O6WRZU4X.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-KN4BZOYH.js.map → mesh.chunk-OSV3EZQ3.js.map} +0 -0
- /package/dist/bin/{mesh.chunk-IU2IWH6O.js.map → mesh.chunk-WODQO6TP.js.map} +0 -0
- /package/dist/bin/{mesh.create-app-NGXACFAU.js.map → mesh.create-app-SOHBL75U.js.map} +0 -0
- /package/dist/bin/{mesh.dev-token-server-YP2VBP2S.js.map → mesh.dev-token-server-SS7QEQOQ.js.map} +0 -0
- /package/dist/bin/{mesh.login-7SIAKFXJ.js.map → mesh.login-6VPMWHHU.js.map} +0 -0
- /package/dist/bin/{mesh.platform-setup-CJXE3BCP.js.map → mesh.platform-setup-DJJB2PWL.js.map} +0 -0
- /package/dist/bin/{mesh.program-BIDQBSSI.js.map → mesh.program-Y4HUNBFP.js.map} +0 -0
- /package/dist/bin/{mesh.registry-7P4MGXEK.js.map → mesh.registry-5P4ZLFLJ.js.map} +0 -0
- /package/dist/bin/{mesh.registry-identity-JW4KO7N7.js.map → mesh.registry-identity-ONGUJQAY.js.map} +0 -0
- /package/dist/bin/{mesh.start-MPQFIEMH.js.map → mesh.start-5CPWOHDJ.js.map} +0 -0
- /package/dist/bin/{mesh.wizard-ZJE4S6PC.js.map → mesh.wizard-FNRTSGVN.js.map} +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export declare const LOCAL_APP_SPICEDB_IMAGE = "authzed/spicedb:v1.42.1";
|
|
2
|
+
export declare const LOCAL_APP_SPICEDB_KEY = "local-dev-key";
|
|
3
|
+
export declare const LOCAL_APP_SPICEDB_LABEL = "mesh.local.app-spicedb";
|
|
4
|
+
export declare const LOCAL_STACK_NETWORK = "mesh-local_mesh_network";
|
|
5
|
+
export type AuthzMode = "policy-engine" | "role-gating";
|
|
6
|
+
export declare function readLocalAuthzConfig(appRoot: string): {
|
|
7
|
+
schema: string;
|
|
8
|
+
prebuilt: boolean;
|
|
9
|
+
} | null;
|
|
10
|
+
export declare function appSpiceDBSlot(app: string): number;
|
|
11
|
+
export declare function appSpiceDBPorts(slot: number): {
|
|
12
|
+
grpc: number;
|
|
13
|
+
http: number;
|
|
14
|
+
};
|
|
15
|
+
export declare const appSpiceDBContainerName: (app: string) => string;
|
|
16
|
+
export declare const appSpiceDBDatabase: (app: string) => string;
|
|
17
|
+
export declare const appSpiceDBSecretName: (tenant: string, env: string, app: string) => string;
|
|
18
|
+
export declare const spicedbInstanceParam: (tenant: string, env: string, instanceId: string) => string;
|
|
19
|
+
export declare const appAuthzParam: (tenant: string, env: string, app: string) => string;
|
|
20
|
+
export declare function instanceRegistryValue(args: {
|
|
21
|
+
app: string;
|
|
22
|
+
tenant: string;
|
|
23
|
+
env: string;
|
|
24
|
+
grpcPort: number;
|
|
25
|
+
httpPort: number;
|
|
26
|
+
}): Record<string, unknown>;
|
|
27
|
+
export declare function buildAppAuthzPointer(args: {
|
|
28
|
+
existing: Record<string, unknown>;
|
|
29
|
+
mode: AuthzMode;
|
|
30
|
+
instanceId?: string;
|
|
31
|
+
zitadel?: {
|
|
32
|
+
projectId: string;
|
|
33
|
+
orgId: string;
|
|
34
|
+
};
|
|
35
|
+
metadataRef: string;
|
|
36
|
+
}): Record<string, unknown>;
|
|
37
|
+
export declare function appSpiceDBEnv(args: {
|
|
38
|
+
grpcPort: number;
|
|
39
|
+
httpPort: number;
|
|
40
|
+
metadataPath: string;
|
|
41
|
+
}): Record<string, string>;
|
|
42
|
+
export declare function spicedbMigrateArgs(app: string): string[];
|
|
43
|
+
export declare function spicedbServeArgs(app: string, ports: {
|
|
44
|
+
grpc: number;
|
|
45
|
+
http: number;
|
|
46
|
+
}): string[];
|
|
47
|
+
export declare function parseDockerPort(output: string): number | null;
|
|
48
|
+
export type DockerResult = {
|
|
49
|
+
status: number | null;
|
|
50
|
+
stdout: string;
|
|
51
|
+
stderr: string;
|
|
52
|
+
};
|
|
53
|
+
export type DockerRunner = (args: string[]) => DockerResult;
|
|
54
|
+
export declare const defaultDocker: DockerRunner;
|
|
55
|
+
export declare function ensureLocalAppSpiceDB(app: string, deps?: {
|
|
56
|
+
docker?: DockerRunner;
|
|
57
|
+
portFree?: (port: number) => Promise<boolean>;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
grpcPort: number;
|
|
60
|
+
httpPort: number;
|
|
61
|
+
created: boolean;
|
|
62
|
+
}>;
|
|
63
|
+
export declare function writeSpiceDBSchema(httpUrl: string, zed: string, opts?: {
|
|
64
|
+
fetchImpl?: typeof fetch;
|
|
65
|
+
attempts?: number;
|
|
66
|
+
delayMs?: number;
|
|
67
|
+
}): Promise<void>;
|
|
68
|
+
export declare function removeLocalAppSpiceDBs(docker?: DockerRunner): string[];
|
|
69
|
+
export type ModuleImporter = (specifier: string, parentFile: string) => Promise<Record<string, any>>;
|
|
70
|
+
export declare const tsxImporter: ModuleImporter;
|
|
71
|
+
export type AppSchema = {
|
|
72
|
+
namespace: string;
|
|
73
|
+
mode?: AuthzMode;
|
|
74
|
+
} & Record<string, unknown>;
|
|
75
|
+
type Compilers = {
|
|
76
|
+
compileOpsHubMetadata: (schema: AppSchema) => unknown;
|
|
77
|
+
compileSpiceDBSchema: (schema: AppSchema) => {
|
|
78
|
+
zed: string;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export declare function loadAppAuthz(appRoot: string, schemaRel: string, importer?: ModuleImporter): Promise<{
|
|
82
|
+
schema: AppSchema;
|
|
83
|
+
compilers: Compilers;
|
|
84
|
+
chunk?: (body: string) => {
|
|
85
|
+
value: string;
|
|
86
|
+
parts: readonly string[];
|
|
87
|
+
};
|
|
88
|
+
}>;
|
|
89
|
+
export type ParamStore = {
|
|
90
|
+
get(name: string): Promise<string | undefined>;
|
|
91
|
+
put(name: string, value: string): Promise<void>;
|
|
92
|
+
};
|
|
93
|
+
export declare const localParamStore: () => Promise<ParamStore>;
|
|
94
|
+
export type PublishLocalAppAuthzDeps = {
|
|
95
|
+
importer?: ModuleImporter;
|
|
96
|
+
params?: ParamStore;
|
|
97
|
+
putSecret?: (name: string, value: Record<string, string>) => Promise<void>;
|
|
98
|
+
ensureSpiceDB?: typeof ensureLocalAppSpiceDB;
|
|
99
|
+
writeSchema?: typeof writeSpiceDBSchema;
|
|
100
|
+
};
|
|
101
|
+
export declare function publishLocalAppAuthz(args: {
|
|
102
|
+
appRoot: string;
|
|
103
|
+
schema: string;
|
|
104
|
+
tenant: string;
|
|
105
|
+
app: string;
|
|
106
|
+
zitadel?: {
|
|
107
|
+
projectId: string;
|
|
108
|
+
orgId: string;
|
|
109
|
+
};
|
|
110
|
+
}, deps?: PublishLocalAppAuthzDeps): Promise<{
|
|
111
|
+
mode: AuthzMode;
|
|
112
|
+
env: Record<string, string>;
|
|
113
|
+
instance?: {
|
|
114
|
+
grpcPort: number;
|
|
115
|
+
httpPort: number;
|
|
116
|
+
};
|
|
117
|
+
}>;
|
|
118
|
+
export {};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { MeshCliError } from "../../utils/errors.js";
|
|
8
|
+
import { upsertLocalSecret, probeTcp } from "./helpers.js";
|
|
9
|
+
import { LOCAL_AWS_CONFIG, LOCAL_ENV } from "./seed.js";
|
|
10
|
+
import { COMPOSE_PROJECT } from "./stack.js";
|
|
11
|
+
export const LOCAL_APP_SPICEDB_IMAGE = "authzed/spicedb:v1.42.1";
|
|
12
|
+
export const LOCAL_APP_SPICEDB_KEY = "local-dev-key";
|
|
13
|
+
export const LOCAL_APP_SPICEDB_LABEL = "mesh.local.app-spicedb";
|
|
14
|
+
export const LOCAL_STACK_NETWORK = `${COMPOSE_PROJECT}_mesh_network`;
|
|
15
|
+
const STACK = "local";
|
|
16
|
+
const GRPC_PORT_BASE = 50100;
|
|
17
|
+
const HTTP_PORT_BASE = 18100;
|
|
18
|
+
const PORT_SLOTS = 100;
|
|
19
|
+
export function readLocalAuthzConfig(appRoot) {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(appRoot, "package.json"), "utf-8"));
|
|
22
|
+
const schema = pkg?.mesh?.authz?.schema;
|
|
23
|
+
if (typeof schema !== "string" || !schema.trim())
|
|
24
|
+
return null;
|
|
25
|
+
return { schema: schema.trim(), prebuilt: pkg?.mesh?.authz?.metadata !== undefined };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const slug = (name, sep) => name.toLowerCase().replace(/[^a-z0-9]+/g, sep).replace(new RegExp(`^\\${sep}+|\\${sep}+$`, "g"), "");
|
|
32
|
+
export function appSpiceDBSlot(app) {
|
|
33
|
+
return createHash("sha256").update(app).digest().readUInt32BE(0) % PORT_SLOTS;
|
|
34
|
+
}
|
|
35
|
+
export function appSpiceDBPorts(slot) {
|
|
36
|
+
const s = ((slot % PORT_SLOTS) + PORT_SLOTS) % PORT_SLOTS;
|
|
37
|
+
return { grpc: GRPC_PORT_BASE + s, http: HTTP_PORT_BASE + s };
|
|
38
|
+
}
|
|
39
|
+
export const appSpiceDBContainerName = (app) => `mesh-local-spicedb-${slug(app, "-")}`;
|
|
40
|
+
export const appSpiceDBDatabase = (app) => `spicedb_${slug(app, "_")}`.slice(0, 63);
|
|
41
|
+
export const appSpiceDBSecretName = (tenant, env, app) => `mesh/${tenant}/${env}/${app}/spicedb/preshared-key`;
|
|
42
|
+
export const spicedbInstanceParam = (tenant, env, instanceId) => `/mesh-platform/${tenant}/${env}/authz/spicedb-instances/${instanceId}`;
|
|
43
|
+
export const appAuthzParam = (tenant, env, app) => `/mesh-platform/${tenant}/${env}/apps/${app}/stacks/${STACK}/authz`;
|
|
44
|
+
export function instanceRegistryValue(args) {
|
|
45
|
+
const secret = appSpiceDBSecretName(args.tenant, args.env, args.app);
|
|
46
|
+
return {
|
|
47
|
+
grpcEndpoint: `localhost:${args.grpcPort}`,
|
|
48
|
+
httpEndpoint: `localhost:${args.httpPort}`,
|
|
49
|
+
namespace: args.app,
|
|
50
|
+
presharedKeySecretName: secret,
|
|
51
|
+
configSecretName: secret,
|
|
52
|
+
owner: { kind: "app", name: args.app },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function buildAppAuthzPointer(args) {
|
|
56
|
+
const existingZitadel = args.existing.zitadel && typeof args.existing.zitadel === "object"
|
|
57
|
+
? args.existing.zitadel
|
|
58
|
+
: {};
|
|
59
|
+
return {
|
|
60
|
+
...args.existing,
|
|
61
|
+
spicedb: {
|
|
62
|
+
instanceRefs: args.mode === "policy-engine" && args.instanceId ? [{ instanceId: args.instanceId }] : [],
|
|
63
|
+
},
|
|
64
|
+
...(args.zitadel || Object.keys(existingZitadel).length > 0
|
|
65
|
+
? { zitadel: { ...existingZitadel, ...(args.zitadel ?? {}) } }
|
|
66
|
+
: {}),
|
|
67
|
+
opsHubMetadataRef: args.metadataRef,
|
|
68
|
+
mode: args.mode,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function appSpiceDBEnv(args) {
|
|
72
|
+
return {
|
|
73
|
+
SPICEDB_ENDPOINT: `localhost:${args.grpcPort}`,
|
|
74
|
+
SPICEDB_HTTP_ENDPOINT: `http://localhost:${args.httpPort}`,
|
|
75
|
+
SPICEDB_PRESHARED_KEY: LOCAL_APP_SPICEDB_KEY,
|
|
76
|
+
SPICEDB_TOKEN: LOCAL_APP_SPICEDB_KEY,
|
|
77
|
+
AUTHZ_METADATA_SSM_PATH: args.metadataPath,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const datastoreUri = (app) => `postgres://postgres:postgres@database:5432/${appSpiceDBDatabase(app)}?sslmode=disable`;
|
|
81
|
+
export function spicedbMigrateArgs(app) {
|
|
82
|
+
return [
|
|
83
|
+
"run", "--rm", "--network", LOCAL_STACK_NETWORK,
|
|
84
|
+
"-e", "SPICEDB_DATASTORE_ENGINE=postgres",
|
|
85
|
+
"-e", `SPICEDB_DATASTORE_CONN_URI=${datastoreUri(app)}`,
|
|
86
|
+
LOCAL_APP_SPICEDB_IMAGE, "datastore", "migrate", "head",
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
export function spicedbServeArgs(app, ports) {
|
|
90
|
+
return [
|
|
91
|
+
"run", "-d",
|
|
92
|
+
"--name", appSpiceDBContainerName(app),
|
|
93
|
+
"--label", `${LOCAL_APP_SPICEDB_LABEL}=${app}`,
|
|
94
|
+
"--restart", "unless-stopped",
|
|
95
|
+
"--network", LOCAL_STACK_NETWORK,
|
|
96
|
+
"-p", `127.0.0.1:${ports.grpc}:50051`,
|
|
97
|
+
"-p", `127.0.0.1:${ports.http}:8443`,
|
|
98
|
+
"-e", `SPICEDB_GRPC_PRESHARED_KEY=${LOCAL_APP_SPICEDB_KEY}`,
|
|
99
|
+
"-e", "SPICEDB_DATASTORE_ENGINE=postgres",
|
|
100
|
+
"-e", `SPICEDB_DATASTORE_CONN_URI=${datastoreUri(app)}`,
|
|
101
|
+
"-e", "SPICEDB_HTTP_ENABLED=true",
|
|
102
|
+
"-e", "SPICEDB_DATASTORE_CONN_POOL_READ_MAX_OPEN=4",
|
|
103
|
+
"-e", "SPICEDB_DATASTORE_CONN_POOL_READ_MIN_OPEN=1",
|
|
104
|
+
"-e", "SPICEDB_DATASTORE_CONN_POOL_WRITE_MAX_OPEN=2",
|
|
105
|
+
"-e", "SPICEDB_DATASTORE_CONN_POOL_WRITE_MIN_OPEN=1",
|
|
106
|
+
LOCAL_APP_SPICEDB_IMAGE, "serve",
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
export function parseDockerPort(output) {
|
|
110
|
+
const match = output.trim().split("\n")[0]?.match(/:(\d+)\s*$/);
|
|
111
|
+
return match ? Number(match[1]) : null;
|
|
112
|
+
}
|
|
113
|
+
export const defaultDocker = (args) => {
|
|
114
|
+
const r = spawnSync("docker", args, { encoding: "utf-8" });
|
|
115
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
116
|
+
};
|
|
117
|
+
const mustDocker = (docker, args, what) => {
|
|
118
|
+
const r = docker(args);
|
|
119
|
+
if (r.status !== 0) {
|
|
120
|
+
throw new MeshCliError(`${what} failed: ${(r.stderr || r.stdout).trim() || `docker exited ${r.status}`}`);
|
|
121
|
+
}
|
|
122
|
+
return r.stdout;
|
|
123
|
+
};
|
|
124
|
+
function ensureDatabase(docker, app) {
|
|
125
|
+
const container = mustDocker(docker, [
|
|
126
|
+
"ps", "-q",
|
|
127
|
+
"--filter", `label=com.docker.compose.project=${COMPOSE_PROJECT}`,
|
|
128
|
+
"--filter", "label=com.docker.compose.service=database",
|
|
129
|
+
], "Finding the local Postgres").trim().split("\n")[0];
|
|
130
|
+
if (!container) {
|
|
131
|
+
throw new MeshCliError("The local platform's Postgres is not running.", {
|
|
132
|
+
remediation: { command: "mesh start" },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const db = appSpiceDBDatabase(app);
|
|
136
|
+
const exists = mustDocker(docker, ["exec", container, "psql", "-U", "postgres", "-tAc", `SELECT 1 FROM pg_database WHERE datname = '${db}'`], `Checking database ${db}`).trim();
|
|
137
|
+
if (exists !== "1") {
|
|
138
|
+
mustDocker(docker, ["exec", container, "psql", "-U", "postgres", "-c", `CREATE DATABASE "${db}"`], `Creating database ${db}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function ensureLocalAppSpiceDB(app, deps = {}) {
|
|
142
|
+
const docker = deps.docker ?? defaultDocker;
|
|
143
|
+
const portFree = deps.portFree ?? (async (port) => !(await probeTcp(port, { timeoutMs: 300 })));
|
|
144
|
+
const name = appSpiceDBContainerName(app);
|
|
145
|
+
const state = docker(["inspect", "-f", "{{.State.Running}}", name]);
|
|
146
|
+
if (state.status === 0) {
|
|
147
|
+
if (state.stdout.trim() !== "true")
|
|
148
|
+
mustDocker(docker, ["start", name], `Starting ${name}`);
|
|
149
|
+
const grpcPort = parseDockerPort(mustDocker(docker, ["port", name, "50051/tcp"], `Reading ${name}'s ports`));
|
|
150
|
+
const httpPort = parseDockerPort(mustDocker(docker, ["port", name, "8443/tcp"], `Reading ${name}'s ports`));
|
|
151
|
+
if (grpcPort && httpPort)
|
|
152
|
+
return { grpcPort, httpPort, created: false };
|
|
153
|
+
mustDocker(docker, ["rm", "-f", name], `Removing ${name}`);
|
|
154
|
+
}
|
|
155
|
+
ensureDatabase(docker, app);
|
|
156
|
+
mustDocker(docker, spicedbMigrateArgs(app), `Migrating ${name}'s datastore`);
|
|
157
|
+
const first = appSpiceDBSlot(app);
|
|
158
|
+
for (let i = 0; i < PORT_SLOTS; i++) {
|
|
159
|
+
const ports = appSpiceDBPorts(first + i);
|
|
160
|
+
if (!(await portFree(ports.grpc)) || !(await portFree(ports.http)))
|
|
161
|
+
continue;
|
|
162
|
+
mustDocker(docker, spicedbServeArgs(app, ports), `Starting ${name}`);
|
|
163
|
+
return { grpcPort: ports.grpc, httpPort: ports.http, created: true };
|
|
164
|
+
}
|
|
165
|
+
throw new MeshCliError(`No free port pair for ${name} in ${GRPC_PORT_BASE}-${GRPC_PORT_BASE + PORT_SLOTS - 1}.`);
|
|
166
|
+
}
|
|
167
|
+
export async function writeSpiceDBSchema(httpUrl, zed, opts = {}) {
|
|
168
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
169
|
+
const attempts = opts.attempts ?? 30;
|
|
170
|
+
let last = "";
|
|
171
|
+
for (let i = 0; i < attempts; i++) {
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetchImpl(`${httpUrl}/v1/schema/write`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: { Authorization: `Bearer ${LOCAL_APP_SPICEDB_KEY}`, "content-type": "application/json" },
|
|
176
|
+
body: JSON.stringify({ schema: zed }),
|
|
177
|
+
});
|
|
178
|
+
if (res.ok)
|
|
179
|
+
return;
|
|
180
|
+
last = `${res.status} ${await res.text()}`;
|
|
181
|
+
if (res.status >= 400 && res.status < 500)
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
last = err instanceof Error ? err.message : String(err);
|
|
186
|
+
}
|
|
187
|
+
await new Promise((r) => setTimeout(r, opts.delayMs ?? 500));
|
|
188
|
+
}
|
|
189
|
+
throw new MeshCliError(`SpiceDB at ${httpUrl} did not accept the app's schema: ${last}`);
|
|
190
|
+
}
|
|
191
|
+
export function removeLocalAppSpiceDBs(docker = defaultDocker) {
|
|
192
|
+
const ids = docker(["ps", "-aq", "--filter", `label=${LOCAL_APP_SPICEDB_LABEL}`]).stdout.trim().split("\n").filter(Boolean);
|
|
193
|
+
if (ids.length > 0)
|
|
194
|
+
docker(["rm", "-f", ...ids]);
|
|
195
|
+
return ids;
|
|
196
|
+
}
|
|
197
|
+
export const tsxImporter = async (specifier, parentFile) => {
|
|
198
|
+
let api;
|
|
199
|
+
try {
|
|
200
|
+
api = createRequire(parentFile).resolve("tsx/esm/api");
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
throw new MeshCliError(`Cannot load the app's authz schema: \`tsx\` is not installed in ${path.dirname(parentFile)}. ` +
|
|
204
|
+
"Add it to the app's devDependencies (`pnpm add -D tsx`).");
|
|
205
|
+
}
|
|
206
|
+
const { tsImport } = (await import(pathToFileURL(api).href));
|
|
207
|
+
return tsImport(specifier, pathToFileURL(parentFile).href);
|
|
208
|
+
};
|
|
209
|
+
const unwrapDefault = (mod) => {
|
|
210
|
+
const d = mod.default ?? mod.schema;
|
|
211
|
+
return d && typeof d === "object" && "default" in d && !("namespace" in d) ? d.default : d;
|
|
212
|
+
};
|
|
213
|
+
export async function loadAppAuthz(appRoot, schemaRel, importer = tsxImporter) {
|
|
214
|
+
const file = path.resolve(appRoot, schemaRel);
|
|
215
|
+
if (!fs.existsSync(file)) {
|
|
216
|
+
throw new MeshCliError(`mesh.authz.schema names ${schemaRel}, which does not exist under ${appRoot}.`);
|
|
217
|
+
}
|
|
218
|
+
const schema = unwrapDefault(await importer(pathToFileURL(file).href, file));
|
|
219
|
+
if (!schema || typeof schema.namespace !== "string") {
|
|
220
|
+
throw new MeshCliError(`${schemaRel} must export the app's defineSchema(...) result as its default export.`);
|
|
221
|
+
}
|
|
222
|
+
const compilers = (await importer("@mesh-tech/authz/compilers", file));
|
|
223
|
+
let chunk;
|
|
224
|
+
try {
|
|
225
|
+
const ssm = await importer("@mesh-tech/authz/metadata/ssm", file);
|
|
226
|
+
if (typeof ssm.chunkSsmValue === "function")
|
|
227
|
+
chunk = ssm.chunkSsmValue;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
}
|
|
231
|
+
return { schema, compilers, chunk };
|
|
232
|
+
}
|
|
233
|
+
export const localParamStore = async () => {
|
|
234
|
+
const { SSMClient, GetParameterCommand, PutParameterCommand } = await import("@aws-sdk/client-ssm");
|
|
235
|
+
const ssm = new SSMClient(LOCAL_AWS_CONFIG);
|
|
236
|
+
return {
|
|
237
|
+
async get(name) {
|
|
238
|
+
try {
|
|
239
|
+
return (await ssm.send(new GetParameterCommand({ Name: name }))).Parameter?.Value;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
async put(name, value) {
|
|
246
|
+
await ssm.send(new PutParameterCommand({ Name: name, Value: value, Type: "String", Tier: "Advanced", Overwrite: true }));
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
export async function publishLocalAppAuthz(args, deps = {}) {
|
|
251
|
+
const env = LOCAL_ENV;
|
|
252
|
+
const { schema, compilers, chunk } = await loadAppAuthz(args.appRoot, args.schema, deps.importer);
|
|
253
|
+
const mode = schema.mode === "role-gating" ? "role-gating" : "policy-engine";
|
|
254
|
+
const params = deps.params ?? (await localParamStore());
|
|
255
|
+
const base = appAuthzParam(args.tenant, env, args.app);
|
|
256
|
+
const metadataRef = `${base}/ops-hub-metadata`;
|
|
257
|
+
let instance;
|
|
258
|
+
if (mode === "policy-engine") {
|
|
259
|
+
const running = await (deps.ensureSpiceDB ?? ensureLocalAppSpiceDB)(args.app);
|
|
260
|
+
instance = { grpcPort: running.grpcPort, httpPort: running.httpPort };
|
|
261
|
+
await (deps.writeSchema ?? writeSpiceDBSchema)(`http://localhost:${instance.httpPort}`, compilers.compileSpiceDBSchema(schema).zed);
|
|
262
|
+
await (deps.putSecret ?? upsertLocalSecret)(appSpiceDBSecretName(args.tenant, env, args.app), {
|
|
263
|
+
PRESHARED_KEY: LOCAL_APP_SPICEDB_KEY,
|
|
264
|
+
});
|
|
265
|
+
await params.put(spicedbInstanceParam(args.tenant, env, args.app), JSON.stringify(instanceRegistryValue({ app: args.app, tenant: args.tenant, env, ...instance })));
|
|
266
|
+
}
|
|
267
|
+
const body = JSON.stringify(compilers.compileOpsHubMetadata(schema));
|
|
268
|
+
const split = chunk ? chunk(body) : { value: body, parts: [] };
|
|
269
|
+
for (const [i, part] of split.parts.entries())
|
|
270
|
+
await params.put(`${metadataRef}/part-${i}`, part);
|
|
271
|
+
await params.put(metadataRef, split.value);
|
|
272
|
+
let existing = {};
|
|
273
|
+
const raw = await params.get(base);
|
|
274
|
+
if (raw) {
|
|
275
|
+
try {
|
|
276
|
+
const parsed = JSON.parse(raw);
|
|
277
|
+
if (parsed && typeof parsed === "object")
|
|
278
|
+
existing = parsed;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
await params.put(base, JSON.stringify(buildAppAuthzPointer({
|
|
284
|
+
existing,
|
|
285
|
+
mode,
|
|
286
|
+
instanceId: instance ? args.app : undefined,
|
|
287
|
+
zitadel: args.zitadel,
|
|
288
|
+
metadataRef,
|
|
289
|
+
})));
|
|
290
|
+
return {
|
|
291
|
+
mode,
|
|
292
|
+
env: instance ? appSpiceDBEnv({ ...instance, metadataPath: `${base}/metadata` }) : {},
|
|
293
|
+
...(instance ? { instance } : {}),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -21,6 +21,7 @@ export declare function reconcileRegistryFromZitadel(): Promise<{
|
|
|
21
21
|
tenants: string[];
|
|
22
22
|
apps: number;
|
|
23
23
|
}>;
|
|
24
|
+
export declare function isZitadelNoChanges(err: unknown): boolean;
|
|
24
25
|
export declare function ensureSignInApp(args: {
|
|
25
26
|
tenant: string;
|
|
26
27
|
app: string;
|
|
@@ -197,6 +197,11 @@ export async function reconcileRegistryFromZitadel() {
|
|
|
197
197
|
}
|
|
198
198
|
return { tenants, apps };
|
|
199
199
|
}
|
|
200
|
+
export function isZitadelNoChanges(err) {
|
|
201
|
+
if (!(err instanceof Error))
|
|
202
|
+
return false;
|
|
203
|
+
return err.status === 400 && /No changes|COMMAND-1m88i/.test(err.message);
|
|
204
|
+
}
|
|
200
205
|
export async function ensureSignInApp(args) {
|
|
201
206
|
const pat = readSeederPat();
|
|
202
207
|
const orgId = await ensureOrg(pat, args.tenant);
|
|
@@ -220,7 +225,13 @@ export async function ensureSignInApp(args) {
|
|
|
220
225
|
};
|
|
221
226
|
const existing = await searchZitadelApp(pat, projectId, name, orgId);
|
|
222
227
|
if (existing) {
|
|
223
|
-
|
|
228
|
+
try {
|
|
229
|
+
await api(pat, "PUT", `/management/v1/projects/${projectId}/apps/${existing.id}/oidc_config`, config, orgId);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
if (!isZitadelNoChanges(err))
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
224
235
|
return;
|
|
225
236
|
}
|
|
226
237
|
const created = await api(pat, "POST", `/management/v1/projects/${projectId}/apps/oidc`, { name, ...config }, orgId);
|
|
@@ -3,6 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { logError, logInfo, logSuccess, logWarn } from "../../utils/log.js";
|
|
5
5
|
import { MeshCliError } from "../../utils/errors.js";
|
|
6
|
+
import { removeLocalAppSpiceDBs } from "./app-authz.js";
|
|
6
7
|
import { compose, COMPOSE_PROJECT, composeStreamed, crashedServices, DEFAULT_HUB_PORT, ensureDockerAvailable, findPackageRoot, hubApiRunning, hubEndpoints, hubOverlayServices, hubPort, idpsOverlayServices, ONE_SHOT_SERVICES, overlayRunning, probeEndpoint, STACK_ENDPOINTS, stackOwnedElsewhere, stackServices, } from "./stack.js";
|
|
7
8
|
import { probeRegistryToken, REGISTRY_LOGIN_FIX } from "../../utils/auth-preflight.js";
|
|
8
9
|
import { ensureHubAuthImage, ensureHubImages, buildHubImagesFromSource, readHubCompiledAuthz, hasRegistryAuth, localHubVersion, planWithHubRefresh, } from "./hub-local.js";
|
|
@@ -314,6 +315,7 @@ export function registerLocalCommands(program) {
|
|
|
314
315
|
if (foreign && !opts.force) {
|
|
315
316
|
throw new MeshCliError(`The running local platform was started from a different checkout (${foreign}) — another project may be using it.`, { remediation: { command: "mesh stop --force # stop it anyway" } });
|
|
316
317
|
}
|
|
318
|
+
removeLocalAppSpiceDBs();
|
|
317
319
|
const aliases = removeAllAliasContainers();
|
|
318
320
|
if (aliases.length > 0)
|
|
319
321
|
logInfo(`Removed ${aliases.length} local docs alias container(s).`);
|
|
@@ -475,7 +475,7 @@ var init_cache_home = __esm({
|
|
|
475
475
|
|
|
476
476
|
// packages/mesh-cli/src/commands/local/stack.ts
|
|
477
477
|
import { parse as parseYaml } from "yaml";
|
|
478
|
-
var DEFAULT_HUB_PORT;
|
|
478
|
+
var COMPOSE_PROJECT, DEFAULT_HUB_PORT;
|
|
479
479
|
var init_stack = __esm({
|
|
480
480
|
"packages/mesh-cli/src/commands/local/stack.ts"() {
|
|
481
481
|
"use strict";
|
|
@@ -483,6 +483,7 @@ var init_stack = __esm({
|
|
|
483
483
|
init_cache_home();
|
|
484
484
|
init_errors();
|
|
485
485
|
init_log();
|
|
486
|
+
COMPOSE_PROJECT = "mesh-local";
|
|
486
487
|
DEFAULT_HUB_PORT = "9000";
|
|
487
488
|
}
|
|
488
489
|
});
|
|
@@ -784,6 +785,19 @@ var init_dev_local = __esm({
|
|
|
784
785
|
}
|
|
785
786
|
});
|
|
786
787
|
|
|
788
|
+
// packages/mesh-cli/src/commands/local/app-authz.ts
|
|
789
|
+
var LOCAL_STACK_NETWORK;
|
|
790
|
+
var init_app_authz = __esm({
|
|
791
|
+
"packages/mesh-cli/src/commands/local/app-authz.ts"() {
|
|
792
|
+
"use strict";
|
|
793
|
+
init_errors();
|
|
794
|
+
init_helpers();
|
|
795
|
+
init_seed();
|
|
796
|
+
init_stack();
|
|
797
|
+
LOCAL_STACK_NETWORK = `${COMPOSE_PROJECT}_mesh_network`;
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
787
801
|
// packages/mesh-cli/src/commands/local/local-spicedb.ts
|
|
788
802
|
var init_local_spicedb = __esm({
|
|
789
803
|
"packages/mesh-cli/src/commands/local/local-spicedb.ts"() {
|
|
@@ -873,6 +887,7 @@ var init_dev = __esm({
|
|
|
873
887
|
init_dev_launch();
|
|
874
888
|
init_dev_local();
|
|
875
889
|
init_auth_provision();
|
|
890
|
+
init_app_authz();
|
|
876
891
|
init_seed();
|
|
877
892
|
init_publish_docs();
|
|
878
893
|
init_act_as_local();
|