@ory/argus 0.14.0 → 1.0.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.
Files changed (150) hide show
  1. package/README.md +31 -46
  2. package/assets/commands/temporal-up.md +1 -1
  3. package/assets/skills/auth-setup/SKILL.md +1 -1
  4. package/assets/skills/local-dev/SKILL.md +17 -7
  5. package/assets/skills/ory-build-agent/SKILL.md +39 -91
  6. package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -17
  7. package/assets/skills/ory-temporal-worker/SKILL.md +31 -33
  8. package/assets/skills/permissions-onboarding/SKILL.md +131 -104
  9. package/dist/adapters.d.ts +88 -32
  10. package/dist/adapters.js +443 -164
  11. package/dist/agent-auth.d.ts +226 -67
  12. package/dist/agent-auth.js +951 -205
  13. package/dist/auth-store.d.ts +37 -2
  14. package/dist/auth-store.js +37 -3
  15. package/dist/auth.d.ts +33 -4
  16. package/dist/auth.js +163 -20
  17. package/dist/bash-parser.d.ts +98 -0
  18. package/dist/bash-parser.js +396 -0
  19. package/dist/branding.d.ts +77 -16
  20. package/dist/branding.js +93 -23
  21. package/dist/build-info.json +4 -4
  22. package/dist/cli-invocation.d.ts +1 -1
  23. package/dist/cli-invocation.js +2 -1
  24. package/dist/cli.d.ts +20 -29
  25. package/dist/cli.js +271 -239
  26. package/dist/client.d.ts +175 -138
  27. package/dist/client.js +672 -391
  28. package/dist/config.d.ts +217 -54
  29. package/dist/config.js +461 -42
  30. package/dist/context.d.ts +10 -0
  31. package/dist/context.js +21 -0
  32. package/dist/contract-suite.d.ts +3 -5
  33. package/dist/contract-suite.js +75 -47
  34. package/dist/denial.d.ts +36 -3
  35. package/dist/denial.js +79 -10
  36. package/dist/event-reporter.d.ts +77 -0
  37. package/dist/event-reporter.js +776 -0
  38. package/dist/external-registrations-main.d.ts +10 -0
  39. package/dist/external-registrations-main.js +38 -0
  40. package/dist/external-registrations.d.ts +79 -0
  41. package/dist/external-registrations.js +188 -0
  42. package/dist/help-cli.d.ts +39 -0
  43. package/dist/help-cli.js +55 -0
  44. package/dist/hook-timeout.d.ts +64 -0
  45. package/dist/hook-timeout.js +88 -0
  46. package/dist/index.d.ts +28 -23
  47. package/dist/index.js +173 -58
  48. package/dist/lifecycle.d.ts +3 -3
  49. package/dist/lifecycle.js +38 -6
  50. package/dist/local/cli.js +11 -6
  51. package/dist/local/configs.d.ts +74 -18
  52. package/dist/local/configs.js +291 -84
  53. package/dist/local/health.js +7 -7
  54. package/dist/local/index.d.ts +2 -2
  55. package/dist/local/index.js +24 -10
  56. package/dist/local/manager.d.ts +20 -1
  57. package/dist/local/manager.js +159 -36
  58. package/dist/local/ports.d.ts +158 -0
  59. package/dist/local/ports.js +443 -0
  60. package/dist/local/seed.d.ts +13 -5
  61. package/dist/local/seed.js +62 -38
  62. package/dist/logger.d.ts +54 -25
  63. package/dist/logger.js +329 -63
  64. package/dist/mcp.d.ts +2 -2
  65. package/dist/mcp.js +10 -5
  66. package/dist/mirror-bootstrap.d.ts +48 -0
  67. package/dist/mirror-bootstrap.js +254 -0
  68. package/dist/opl.d.ts +289 -0
  69. package/dist/opl.js +446 -0
  70. package/dist/permission-mode.d.ts +87 -0
  71. package/dist/permission-mode.js +307 -0
  72. package/dist/permissions-cli.d.ts +13 -49
  73. package/dist/permissions-cli.js +154 -348
  74. package/dist/permissions.d.ts +148 -38
  75. package/dist/permissions.js +591 -45
  76. package/dist/post-install.d.ts +33 -0
  77. package/dist/post-install.js +127 -0
  78. package/dist/read-credential.d.ts +65 -0
  79. package/dist/read-credential.js +86 -0
  80. package/dist/registry/cli.js +5 -2
  81. package/dist/registry/config.d.ts +0 -17
  82. package/dist/registry/config.js +0 -23
  83. package/dist/registry/index.d.ts +1 -1
  84. package/dist/registry/index.js +2 -2
  85. package/dist/registry/manager.d.ts +4 -21
  86. package/dist/registry/manager.js +21 -37
  87. package/dist/runtime-credential.d.ts +140 -0
  88. package/dist/runtime-credential.js +572 -0
  89. package/dist/runtime.d.ts +408 -0
  90. package/dist/runtime.js +748 -0
  91. package/dist/setup.d.ts +23 -47
  92. package/dist/setup.js +59 -116
  93. package/dist/skills.js +0 -7
  94. package/dist/status-cli.d.ts +27 -11
  95. package/dist/status-cli.js +121 -115
  96. package/dist/status-data.d.ts +116 -17
  97. package/dist/status-data.js +121 -38
  98. package/dist/subject.d.ts +126 -20
  99. package/dist/subject.js +215 -30
  100. package/dist/testing.d.ts +74 -38
  101. package/dist/testing.js +185 -68
  102. package/dist/tool-catalog.d.ts +53 -11
  103. package/dist/tool-catalog.js +164 -13
  104. package/dist/tool-metadata.d.ts +7 -6
  105. package/dist/tool-metadata.js +6 -5
  106. package/dist/types.d.ts +11 -1
  107. package/dist/uninstall.d.ts +71 -34
  108. package/dist/uninstall.js +217 -85
  109. package/dist/user-login.d.ts +9 -10
  110. package/dist/user-login.js +56 -75
  111. package/dist/watch-cli.d.ts +6 -0
  112. package/dist/watch-cli.js +217 -0
  113. package/package.json +4 -27
  114. package/assets/commands/dashboard.md +0 -34
  115. package/dist/dashboard-cli.d.ts +0 -8
  116. package/dist/dashboard-cli.js +0 -70
  117. package/dist/dev.d.ts +0 -103
  118. package/dist/dev.js +0 -583
  119. package/dist/interactive-setup.d.ts +0 -286
  120. package/dist/interactive-setup.js +0 -1734
  121. package/dist/local/jaeger-main.d.ts +0 -13
  122. package/dist/local/jaeger-main.js +0 -85
  123. package/dist/local/jaeger.d.ts +0 -50
  124. package/dist/local/jaeger.js +0 -162
  125. package/dist/otel/exporter.d.ts +0 -17
  126. package/dist/otel/exporter.js +0 -12
  127. package/dist/otel/index.d.ts +0 -2
  128. package/dist/otel/index.js +0 -8
  129. package/dist/otel/otlp.d.ts +0 -103
  130. package/dist/otel/otlp.js +0 -385
  131. package/dist/project-api-key.d.ts +0 -69
  132. package/dist/project-api-key.js +0 -147
  133. package/dist/setup-actions.d.ts +0 -232
  134. package/dist/setup-actions.js +0 -507
  135. package/dist/tracer.d.ts +0 -190
  136. package/dist/tracer.js +0 -481
  137. package/dist/watch-sandbox.d.ts +0 -9
  138. package/dist/watch-sandbox.js +0 -81
  139. package/dist/web/api.d.ts +0 -33
  140. package/dist/web/api.js +0 -294
  141. package/dist/web/launch.d.ts +0 -11
  142. package/dist/web/launch.js +0 -96
  143. package/dist/web/server.d.ts +0 -20
  144. package/dist/web/server.js +0 -233
  145. package/dist/web/types.d.ts +0 -65
  146. package/dist/web/types.js +0 -2
  147. package/dist/webapp/assets/index-Wucl4SZs.css +0 -1
  148. package/dist/webapp/assets/index-m-GtEdq0.js +0 -49
  149. package/dist/webapp/favicon.ico +0 -0
  150. package/dist/webapp/index.html +0 -15
@@ -0,0 +1,443 @@
1
+ "use strict";
2
+ /**
3
+ * Host-facing addressing for the local Ory stack — which ports it publishes on
4
+ * the host, and which host name everything else should use to reach it.
5
+ *
6
+ * Two kinds of port live in this stack, and conflating them is the bug this
7
+ * module exists to prevent:
8
+ *
9
+ * - **Container-internal ports** are what each service listens on *inside* the
10
+ * compose network, and what one service uses to address another
11
+ * (`http://kratos:4433`). They live on a private bridge network, so they can
12
+ * never collide with anything on the host and there is no reason to make them
13
+ * configurable. They are fixed constants below.
14
+ * - **Host ports** are the left-hand side of a compose `ports:` mapping — the
15
+ * only ports that are actually claimed on the developer's machine, and so the
16
+ * only ones that can collide. Every one of them is overridable.
17
+ *
18
+ * The defaults are the stock upstream Ory ports (4433/4434, 4444/4445,
19
+ * 4466/4467, …), which is deliberate — they are the ports every Ory doc and
20
+ * tutorial uses. It also means a developer who already runs Ory locally, which
21
+ * is precisely this stack's audience, collides on first run. Before #63 there
22
+ * was no override and no diagnosis: `docker compose up` failed with Docker's
23
+ * raw "port is already allocated" and the only apparent fix was to hand-edit
24
+ * the generated compose file, which the next `local up` regenerates away.
25
+ *
26
+ * Three levels of control, cheapest first:
27
+ *
28
+ * 1. `ORY_LOCAL_PORT_OFFSET=100` shifts *every* default by a constant, which is
29
+ * the common case ("something else owns the Ory ports, move the whole
30
+ * stack"). Relative spacing is preserved, so the set stays collision-free
31
+ * with itself.
32
+ * 2. `ORY_LOCAL_<SERVICE>_PORT` pins one service exactly, and wins over the
33
+ * offset.
34
+ * 3. `ORY_LOCAL_HOST` changes the host every generated URL advertises, for a
35
+ * remote Docker daemon or an agent running inside a container. It defaults
36
+ * to the host parsed out of `DOCKER_HOST` when that points somewhere other
37
+ * than this machine, so the remote-daemon case usually needs no flag at all.
38
+ */
39
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
40
+ if (k2 === undefined) k2 = k;
41
+ var desc = Object.getOwnPropertyDescriptor(m, k);
42
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
43
+ desc = { enumerable: true, get: function() { return m[k]; } };
44
+ }
45
+ Object.defineProperty(o, k2, desc);
46
+ }) : (function(o, m, k, k2) {
47
+ if (k2 === undefined) k2 = k;
48
+ o[k2] = m[k];
49
+ }));
50
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
51
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
52
+ }) : function(o, v) {
53
+ o["default"] = v;
54
+ });
55
+ var __importStar = (this && this.__importStar) || (function () {
56
+ var ownKeys = function(o) {
57
+ ownKeys = Object.getOwnPropertyNames || function (o) {
58
+ var ar = [];
59
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
60
+ return ar;
61
+ };
62
+ return ownKeys(o);
63
+ };
64
+ return function (mod) {
65
+ if (mod && mod.__esModule) return mod;
66
+ var result = {};
67
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
68
+ __setModuleDefault(result, mod);
69
+ return result;
70
+ };
71
+ })();
72
+ Object.defineProperty(exports, "__esModule", { value: true });
73
+ exports.DEFAULT_LOCAL_HOST = exports.SKIP_PORT_CHECK_ENV_VAR = exports.LOCAL_HOST_ENV_VAR = exports.PORT_OFFSET_ENV_VAR = exports.LOCAL_PORT_SPECS = exports.LOCAL_PORT_NAMES = exports.CONSOLE_CONTAINER_PORT = exports.OATHKEEPER_API_CONTAINER_PORT = exports.OATHKEEPER_PROXY_CONTAINER_PORT = exports.LOGIN_UI_CONTAINER_PORT = exports.GATEWAY_CONTAINER_PORT = exports.HYDRA_TOKEN_USER_CONTAINER_PORT = exports.HYDRA_ADMIN_CONTAINER_PORT = exports.HYDRA_PUBLIC_CONTAINER_PORT = exports.KETO_WRITE_CONTAINER_PORT = exports.KETO_READ_CONTAINER_PORT = exports.KRATOS_ADMIN_CONTAINER_PORT = exports.KRATOS_PUBLIC_CONTAINER_PORT = void 0;
74
+ exports.portCheckDisabled = portCheckDisabled;
75
+ exports.resolveLocalPorts = resolveLocalPorts;
76
+ exports.hostFromDockerHost = hostFromDockerHost;
77
+ exports.isLoopbackHost = isLoopbackHost;
78
+ exports.resolveLocalHost = resolveLocalHost;
79
+ exports.isLocalDockerDaemon = isLocalDockerDaemon;
80
+ exports.hostUrl = hostUrl;
81
+ exports.isPortAvailable = isPortAvailable;
82
+ exports.findPortConflicts = findPortConflicts;
83
+ exports.describePortConflicts = describePortConflicts;
84
+ const net = __importStar(require("node:net"));
85
+ // ─── Container-internal ports (fixed) ──────────────────────────────
86
+ //
87
+ // Changing any of these would also mean rewriting the service's own listen
88
+ // config and every cross-service URL on the compose network, for no benefit:
89
+ // nothing outside the bridge network can see them.
90
+ exports.KRATOS_PUBLIC_CONTAINER_PORT = 4433;
91
+ exports.KRATOS_ADMIN_CONTAINER_PORT = 4434;
92
+ exports.KETO_READ_CONTAINER_PORT = 4466;
93
+ exports.KETO_WRITE_CONTAINER_PORT = 4467;
94
+ exports.HYDRA_PUBLIC_CONTAINER_PORT = 4444;
95
+ exports.HYDRA_ADMIN_CONTAINER_PORT = 4445;
96
+ exports.HYDRA_TOKEN_USER_CONTAINER_PORT = 5555;
97
+ exports.GATEWAY_CONTAINER_PORT = 4000;
98
+ exports.LOGIN_UI_CONTAINER_PORT = 4455;
99
+ exports.OATHKEEPER_PROXY_CONTAINER_PORT = 4456;
100
+ exports.OATHKEEPER_API_CONTAINER_PORT = 4457;
101
+ /** Console Lite's Dockerfile sets `ENV PORT=3000`. */
102
+ exports.CONSOLE_CONTAINER_PORT = 3000;
103
+ // ─── Host ports (overridable) ──────────────────────────────────────
104
+ /** Every host port the stack publishes, in the order `local status` lists them. */
105
+ exports.LOCAL_PORT_NAMES = [
106
+ "gateway",
107
+ "console",
108
+ "loginUi",
109
+ "kratosPublic",
110
+ "kratosAdmin",
111
+ "ketoRead",
112
+ "ketoWrite",
113
+ "hydraPublic",
114
+ "hydraAdmin",
115
+ "hydraTokenUser",
116
+ "oathkeeperProxy",
117
+ "oathkeeperApi",
118
+ ];
119
+ /**
120
+ * The port table. `default` doubles as the container-internal port for every
121
+ * service, which is what keeps the generated mapping readable
122
+ * (`"4433:4433"` by default, `"5433:4433"` once overridden).
123
+ */
124
+ exports.LOCAL_PORT_SPECS = {
125
+ gateway: {
126
+ default: exports.GATEWAY_CONTAINER_PORT,
127
+ envVar: "ORY_LOCAL_GATEWAY_PORT",
128
+ label: "API gateway",
129
+ },
130
+ console: {
131
+ default: 4100,
132
+ envVar: "ORY_LOCAL_CONSOLE_PORT",
133
+ label: "Ory Console",
134
+ },
135
+ loginUi: {
136
+ default: exports.LOGIN_UI_CONTAINER_PORT,
137
+ envVar: "ORY_LOCAL_LOGIN_UI_PORT",
138
+ label: "Login UI",
139
+ },
140
+ kratosPublic: {
141
+ default: exports.KRATOS_PUBLIC_CONTAINER_PORT,
142
+ envVar: "ORY_LOCAL_KRATOS_PUBLIC_PORT",
143
+ label: "Kratos (public)",
144
+ },
145
+ kratosAdmin: {
146
+ default: exports.KRATOS_ADMIN_CONTAINER_PORT,
147
+ envVar: "ORY_LOCAL_KRATOS_ADMIN_PORT",
148
+ label: "Kratos (admin)",
149
+ },
150
+ ketoRead: {
151
+ default: exports.KETO_READ_CONTAINER_PORT,
152
+ envVar: "ORY_LOCAL_KETO_READ_PORT",
153
+ label: "Keto (read)",
154
+ },
155
+ ketoWrite: {
156
+ default: exports.KETO_WRITE_CONTAINER_PORT,
157
+ envVar: "ORY_LOCAL_KETO_WRITE_PORT",
158
+ label: "Keto (write)",
159
+ },
160
+ hydraPublic: {
161
+ default: exports.HYDRA_PUBLIC_CONTAINER_PORT,
162
+ envVar: "ORY_LOCAL_HYDRA_PUBLIC_PORT",
163
+ label: "Hydra (public)",
164
+ },
165
+ hydraAdmin: {
166
+ default: exports.HYDRA_ADMIN_CONTAINER_PORT,
167
+ envVar: "ORY_LOCAL_HYDRA_ADMIN_PORT",
168
+ label: "Hydra (admin)",
169
+ },
170
+ hydraTokenUser: {
171
+ default: exports.HYDRA_TOKEN_USER_CONTAINER_PORT,
172
+ envVar: "ORY_LOCAL_HYDRA_TOKEN_USER_PORT",
173
+ label: "Hydra (token user)",
174
+ },
175
+ oathkeeperProxy: {
176
+ default: exports.OATHKEEPER_PROXY_CONTAINER_PORT,
177
+ envVar: "ORY_LOCAL_OATHKEEPER_PROXY_PORT",
178
+ label: "Oathkeeper (proxy)",
179
+ },
180
+ oathkeeperApi: {
181
+ default: exports.OATHKEEPER_API_CONTAINER_PORT,
182
+ envVar: "ORY_LOCAL_OATHKEEPER_API_PORT",
183
+ label: "Oathkeeper (API)",
184
+ },
185
+ };
186
+ /** Shifts every default port by a constant. Overridden per-port by the specific var. */
187
+ exports.PORT_OFFSET_ENV_VAR = "ORY_LOCAL_PORT_OFFSET";
188
+ /** Overrides the host name every generated URL advertises. */
189
+ exports.LOCAL_HOST_ENV_VAR = "ORY_LOCAL_HOST";
190
+ /**
191
+ * Skips every port probe — the pre-start preflight *and* the post-failure
192
+ * diagnosis.
193
+ *
194
+ * Both open real sockets, which is the right thing before starting containers
195
+ * and the wrong thing in a unit test (the core suite sets this so no test
196
+ * depends on which ports happen to be free on the machine running it) or in a
197
+ * sandbox that forbids listening. The sandbox case is why this covers the
198
+ * diagnosis too: where binding is denied, `isPortAvailable` answers "taken" for
199
+ * every port, so an unrelated startup failure would otherwise be reported as a
200
+ * confident and completely wrong "every port is in use".
201
+ */
202
+ exports.SKIP_PORT_CHECK_ENV_VAR = "ORY_LOCAL_SKIP_PORT_CHECK";
203
+ function portCheckDisabled(env = process.env) {
204
+ const raw = env[exports.SKIP_PORT_CHECK_ENV_VAR]?.trim().toLowerCase();
205
+ if (!raw)
206
+ return false;
207
+ return raw !== "0" && raw !== "false" && raw !== "no" && raw !== "off";
208
+ }
209
+ const MIN_PORT = 1;
210
+ const MAX_PORT = 65535;
211
+ function parsePort(raw) {
212
+ // Reject anything that isn't a bare integer — `"4000x"`, `"40.5"`, `" "`.
213
+ // parseInt would happily accept the first two and produce a port the user
214
+ // did not ask for, which is worse than ignoring the value.
215
+ if (!/^\d+$/.test(raw))
216
+ return undefined;
217
+ const value = Number(raw);
218
+ if (!Number.isInteger(value) || value < MIN_PORT || value > MAX_PORT) {
219
+ return undefined;
220
+ }
221
+ return value;
222
+ }
223
+ /**
224
+ * Resolve every host port from the environment.
225
+ *
226
+ * Invalid values are **ignored with a warning** rather than throwing: this runs
227
+ * on the way into `local up`, and refusing to start because one env var has a
228
+ * typo would be a worse outcome than starting on the documented default and
229
+ * saying so. Warnings are returned rather than printed so the caller controls
230
+ * the output stream.
231
+ */
232
+ function resolveLocalPorts(env = process.env) {
233
+ const warnings = [];
234
+ let offset = 0;
235
+ const rawOffset = env[exports.PORT_OFFSET_ENV_VAR]?.trim();
236
+ if (rawOffset) {
237
+ const parsed = /^-?\d+$/.test(rawOffset) ? Number(rawOffset) : undefined;
238
+ if (parsed === undefined) {
239
+ warnings.push(`${exports.PORT_OFFSET_ENV_VAR}="${rawOffset}" is not an integer — ignoring it and using the default ports.`);
240
+ }
241
+ else {
242
+ offset = parsed;
243
+ }
244
+ }
245
+ const ports = {};
246
+ for (const name of exports.LOCAL_PORT_NAMES) {
247
+ const spec = exports.LOCAL_PORT_SPECS[name];
248
+ const raw = env[spec.envVar]?.trim();
249
+ if (raw) {
250
+ const parsed = parsePort(raw);
251
+ if (parsed === undefined) {
252
+ warnings.push(`${spec.envVar}="${raw}" is not a port between ${MIN_PORT} and ${MAX_PORT} — using ${spec.default} for ${spec.label}.`);
253
+ ports[name] = spec.default;
254
+ }
255
+ else {
256
+ ports[name] = parsed;
257
+ }
258
+ continue;
259
+ }
260
+ if (offset !== 0) {
261
+ const shifted = spec.default + offset;
262
+ if (shifted < MIN_PORT || shifted > MAX_PORT) {
263
+ warnings.push(`${exports.PORT_OFFSET_ENV_VAR}=${offset} puts ${spec.label} at ${shifted}, outside ${MIN_PORT}-${MAX_PORT} — using ${spec.default}. Set ${spec.envVar} to place it explicitly.`);
264
+ ports[name] = spec.default;
265
+ }
266
+ else {
267
+ ports[name] = shifted;
268
+ }
269
+ continue;
270
+ }
271
+ ports[name] = spec.default;
272
+ }
273
+ warnings.push(...describeDuplicatePorts(ports));
274
+ return { ports, warnings };
275
+ }
276
+ /**
277
+ * Two services configured onto the same host port. Only reachable through
278
+ * explicit per-port overrides — the offset preserves the spacing of the
279
+ * defaults — and it would otherwise surface as a confusing Docker error about
280
+ * a port the user *did* mean to claim.
281
+ */
282
+ function describeDuplicatePorts(ports) {
283
+ const byPort = new Map();
284
+ for (const name of exports.LOCAL_PORT_NAMES) {
285
+ const list = byPort.get(ports[name]) ?? [];
286
+ list.push(name);
287
+ byPort.set(ports[name], list);
288
+ }
289
+ const warnings = [];
290
+ for (const [port, names] of byPort) {
291
+ if (names.length < 2)
292
+ continue;
293
+ const labels = names.map((n) => exports.LOCAL_PORT_SPECS[n].label).join(" and ");
294
+ const vars = names.map((n) => exports.LOCAL_PORT_SPECS[n].envVar).join(" / ");
295
+ warnings.push(`${labels} are both configured on host port ${port} — the stack cannot start. Set ${vars} to different values.`);
296
+ }
297
+ return warnings;
298
+ }
299
+ exports.DEFAULT_LOCAL_HOST = "localhost";
300
+ /**
301
+ * Pull the host out of a `DOCKER_HOST` value.
302
+ *
303
+ * `unix://` and `npipe://` sockets are this machine, so they yield nothing and
304
+ * the default stands. A `tcp://` or `ssh://` daemon publishes the stack's ports
305
+ * on *its* host, not ours, so that host is what generated URLs have to
306
+ * advertise — otherwise every URL we print and persist points at a machine that
307
+ * isn't running the stack.
308
+ */
309
+ function hostFromDockerHost(dockerHost) {
310
+ const raw = dockerHost?.trim();
311
+ if (!raw)
312
+ return undefined;
313
+ if (raw.startsWith("unix://") || raw.startsWith("npipe://"))
314
+ return undefined;
315
+ let hostname;
316
+ try {
317
+ hostname = new URL(raw).hostname;
318
+ }
319
+ catch {
320
+ return undefined;
321
+ }
322
+ if (!hostname)
323
+ return undefined;
324
+ // A tcp:// daemon on loopback is still this machine — `localhost` reads
325
+ // better in a banner than `127.0.0.1`, and keeps URLs stable for anyone who
326
+ // sets DOCKER_HOST explicitly at the default address.
327
+ if (isLoopbackHost(hostname))
328
+ return undefined;
329
+ return hostname;
330
+ }
331
+ function isLoopbackHost(host) {
332
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
333
+ return h === "localhost" || h === "127.0.0.1" || h === "::1";
334
+ }
335
+ /**
336
+ * The host name every generated URL should advertise: config files, banners,
337
+ * the health probe, and the `projectUrl` that `local configure` persists.
338
+ */
339
+ function resolveLocalHost(env = process.env) {
340
+ const explicit = env[exports.LOCAL_HOST_ENV_VAR]?.trim();
341
+ if (explicit)
342
+ return { host: explicit, source: "env" };
343
+ const fromDocker = hostFromDockerHost(env.DOCKER_HOST);
344
+ if (fromDocker)
345
+ return { host: fromDocker, source: "docker-host" };
346
+ return { host: exports.DEFAULT_LOCAL_HOST, source: "default" };
347
+ }
348
+ /**
349
+ * True when the Docker daemon publishes ports on *this* machine, which is the
350
+ * only case where probing a local socket says anything about whether the stack
351
+ * will be able to bind. With a remote daemon the probe would test the wrong
352
+ * host, so callers skip the preflight entirely rather than report a conflict
353
+ * that isn't there (or miss one that is).
354
+ */
355
+ function isLocalDockerDaemon(env = process.env) {
356
+ return hostFromDockerHost(env.DOCKER_HOST) === undefined;
357
+ }
358
+ /** Build an `http://host:port` origin, bracketing bare IPv6 literals. */
359
+ function hostUrl(host, port) {
360
+ const needsBrackets = host.includes(":") && !host.startsWith("[");
361
+ return `http://${needsBrackets ? `[${host}]` : host}:${port}`;
362
+ }
363
+ /**
364
+ * Can we bind this port? Docker publishes on all interfaces, so we test the
365
+ * same thing it will do. A port held on loopback alone still fails a
366
+ * wildcard bind, so this catches both.
367
+ *
368
+ * Anything other than a clean listen counts as unavailable: `EADDRINUSE` is the
369
+ * case we are looking for, and `EACCES` (a privileged port without rights)
370
+ * would fail the container just the same.
371
+ */
372
+ function isPortAvailable(port, timeoutMs = 500) {
373
+ return new Promise((resolve) => {
374
+ const server = net.createServer();
375
+ let settled = false;
376
+ const finish = (available) => {
377
+ if (settled)
378
+ return;
379
+ settled = true;
380
+ clearTimeout(timer);
381
+ server.removeAllListeners();
382
+ try {
383
+ server.close();
384
+ }
385
+ catch {
386
+ /* already closed */
387
+ }
388
+ resolve(available);
389
+ };
390
+ const timer = setTimeout(() => finish(true), timeoutMs);
391
+ timer.unref?.();
392
+ server.once("error", () => finish(false));
393
+ server.once("listening", () => finish(true));
394
+ try {
395
+ server.listen({ port, host: "0.0.0.0", exclusive: true });
396
+ }
397
+ catch {
398
+ finish(false);
399
+ }
400
+ });
401
+ }
402
+ /**
403
+ * Which of the stack's host ports are already taken. Probed concurrently —
404
+ * twelve sequential binds would add noticeable latency to every `local up`.
405
+ */
406
+ async function findPortConflicts(ports) {
407
+ const checks = exports.LOCAL_PORT_NAMES.map(async (name) => {
408
+ const available = await isPortAvailable(ports[name]);
409
+ return available
410
+ ? undefined
411
+ : {
412
+ name,
413
+ port: ports[name],
414
+ label: exports.LOCAL_PORT_SPECS[name].label,
415
+ envVar: exports.LOCAL_PORT_SPECS[name].envVar,
416
+ };
417
+ });
418
+ return (await Promise.all(checks)).filter((c) => c !== undefined);
419
+ }
420
+ /**
421
+ * The message a user gets instead of Docker's "port is already allocated":
422
+ * which service wanted which port, and the two ways to move it.
423
+ */
424
+ function describePortConflicts(conflicts) {
425
+ if (conflicts.length === 0)
426
+ return "";
427
+ const lines = [
428
+ conflicts.length === 1
429
+ ? "Port already in use — the local Ory stack cannot start:"
430
+ : `${conflicts.length} ports already in use — the local Ory stack cannot start:`,
431
+ "",
432
+ ...conflicts.map((c) => ` ${c.port} ${c.label} (override with ${c.envVar})`),
433
+ "",
434
+ "The defaults are the stock Ory ports, so another Ory instance is the usual cause.",
435
+ "Move the whole stack out of the way with a single offset:",
436
+ "",
437
+ ` ${exports.PORT_OFFSET_ENV_VAR}=100 <command>`,
438
+ "",
439
+ "or pin individual services with the variables listed above. Do not edit the",
440
+ "generated compose file — the next 'local up' regenerates it.",
441
+ ];
442
+ return lines.join("\n");
443
+ }
@@ -22,12 +22,16 @@
22
22
  * recreates OAuth2 clients (so the secret is always known).
23
23
  */
24
24
  /**
25
- * Stable client id Hydra issues to the seeded user PKCE client. Stable
26
- * so `local configure` can persist it to the shared config and downstream
27
- * runs can rely on `ORY_OAUTH2_CLIENT_ID` resolution without first
25
+ * The client id Hydra issues to the seeded user PKCE client: the same
26
+ * **reserved** id Ory Network's Agent Security setup uses, so local dev and a
27
+ * hosted project name the shared login client identically and the id — never a
28
+ * `client_name` — is what resolves it in both.
29
+ *
30
+ * Stable, so `local configure` can persist it to the shared config and
31
+ * downstream runs can rely on `ORY_OAUTH2_CLIENT_ID` resolution without first
28
32
  * dragging the value out of the `local up` banner.
29
33
  */
30
- export declare const USER_CLIENT_ID = "ory-user-local";
34
+ export declare const USER_CLIENT_ID = "ory-agent-security-login";
31
35
  export interface SeededIdentity {
32
36
  id: string;
33
37
  email: string;
@@ -56,7 +60,11 @@ export interface SeedResult {
56
60
  tuples: number;
57
61
  };
58
62
  }
59
- /** Subject namespace for user identities in seeded permission tuples. */
63
+ /**
64
+ * Subject namespace for user identities in seeded permission tuples. Re-exported
65
+ * from the core default so the seed, the runtime resolver, and the persisted
66
+ * install config all address the user under the same namespace.
67
+ */
60
68
  export declare const USER_SUBJECT_NAMESPACE = "User";
61
69
  /**
62
70
  * Run the full seed process. Idempotent — safe to run multiple times.
@@ -26,26 +26,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.USER_SUBJECT_NAMESPACE = exports.USER_CLIENT_ID = void 0;
27
27
  exports.seedLocalEnvironment = seedLocalEnvironment;
28
28
  const auth_js_1 = require("../auth.js");
29
+ const branding_js_1 = require("../branding.js");
29
30
  const tool_catalog_js_1 = require("../tool-catalog.js");
31
+ const subject_js_1 = require("../subject.js");
32
+ const opl_js_1 = require("../opl.js");
30
33
  const configs_js_1 = require("./configs.js");
34
+ const ports_js_1 = require("./ports.js");
31
35
  const health_js_1 = require("./health.js");
32
- const KRATOS_ADMIN = `http://localhost:${configs_js_1.KRATOS_ADMIN_PORT}`;
33
- const KETO_WRITE = `http://localhost:${configs_js_1.KETO_WRITE_PORT}`;
34
- const HYDRA_ADMIN = `http://localhost:${configs_js_1.HYDRA_ADMIN_PORT}`;
36
+ const KRATOS_ADMIN = (0, ports_js_1.hostUrl)(configs_js_1.LOCAL_STACK_HOST, configs_js_1.KRATOS_ADMIN_PORT);
37
+ const KETO_WRITE = (0, ports_js_1.hostUrl)(configs_js_1.LOCAL_STACK_HOST, configs_js_1.KETO_WRITE_PORT);
38
+ const HYDRA_ADMIN = (0, ports_js_1.hostUrl)(configs_js_1.LOCAL_STACK_HOST, configs_js_1.HYDRA_ADMIN_PORT);
35
39
  const USER_EMAIL = "user@ory-local.dev";
36
40
  const USER_PASSWORD = "ory-user-local-dev-password!";
37
- const USER_CLIENT_NAME = "ory-agent-plugins-local-user";
38
41
  /**
39
- * Stable client id Hydra issues to the seeded user PKCE client. Stable
40
- * so `local configure` can persist it to the shared config and downstream
41
- * runs can rely on `ORY_OAUTH2_CLIENT_ID` resolution without first
42
+ * The client id Hydra issues to the seeded user PKCE client: the same
43
+ * **reserved** id Ory Network's Agent Security setup uses, so local dev and a
44
+ * hosted project name the shared login client identically and the id — never a
45
+ * `client_name` — is what resolves it in both.
46
+ *
47
+ * Stable, so `local configure` can persist it to the shared config and
48
+ * downstream runs can rely on `ORY_OAUTH2_CLIENT_ID` resolution without first
42
49
  * dragging the value out of the `local up` banner.
43
50
  */
44
- exports.USER_CLIENT_ID = "ory-user-local";
51
+ exports.USER_CLIENT_ID = branding_js_1.USER_LOGIN_CLIENT_ID;
45
52
  /**
46
- * Tools the dev launcher seeds tuples for. Sourced from the shared
47
- * per-harness catalog so a single seed grants the local user identity
48
- * `use` on every harness's built-in tool set.
53
+ * Tools the explicit local-stack command seeds tuples for. Sourced from the
54
+ * shared catalog so one seed covers every harness's built-in tool set.
49
55
  */
50
56
  const COMMON_TOOLS = tool_catalog_js_1.ALL_TOOLS;
51
57
  async function jsonFetch(url, opts = {}) {
@@ -82,8 +88,12 @@ async function seedIdentity(email, password, name) {
82
88
  }));
83
89
  return { id: identity.id, email };
84
90
  }
85
- /** Subject namespace for user identities in seeded permission tuples. */
86
- exports.USER_SUBJECT_NAMESPACE = "User";
91
+ /**
92
+ * Subject namespace for user identities in seeded permission tuples. Re-exported
93
+ * from the core default so the seed, the runtime resolver, and the persisted
94
+ * install config all address the user under the same namespace.
95
+ */
96
+ exports.USER_SUBJECT_NAMESPACE = subject_js_1.DEFAULT_USER_SUBJECT_NAMESPACE;
87
97
  /**
88
98
  * Write permission tuples granting the user identity access to all
89
99
  * common tools. The subject is written as a SubjectSet
@@ -91,16 +101,16 @@ exports.USER_SUBJECT_NAMESPACE = "User";
91
101
  * dialog — which only emits SubjectSet subjects — can recreate the same
92
102
  * tuple by hand during the runbook walkthrough.
93
103
  */
94
- async function seedPermissions(userId, namespace) {
104
+ async function seedPermissions(userId, namespace, objects = COMMON_TOOLS) {
95
105
  let created = 0;
96
- for (const tool of COMMON_TOOLS) {
106
+ for (const object of objects) {
97
107
  try {
98
108
  await jsonFetch(`${KETO_WRITE}/admin/relation-tuples`, {
99
109
  method: "PUT",
100
110
  body: JSON.stringify({
101
111
  namespace,
102
- object: tool,
103
- relation: "use",
112
+ object,
113
+ relation: opl_js_1.RELATION_USERS,
104
114
  subject_set: {
105
115
  namespace: exports.USER_SUBJECT_NAMESPACE,
106
116
  object: userId,
@@ -113,7 +123,7 @@ async function seedPermissions(userId, namespace) {
113
123
  catch (err) {
114
124
  const msg = err instanceof Error ? err.message : String(err);
115
125
  if (!msg.includes("409")) {
116
- process.stderr.write(` Warning: failed to create permission for ${tool}: ${msg}\n`);
126
+ process.stderr.write(` Warning: failed to create permission for ${object}: ${msg}\n`);
117
127
  }
118
128
  else {
119
129
  created++;
@@ -123,25 +133,29 @@ async function seedPermissions(userId, namespace) {
123
133
  return created;
124
134
  }
125
135
  /**
126
- * Delete a Hydra OAuth2 client if it exists, then create it with a
127
- * known client_id/secret. We always recreate so the launcher knows the
128
- * exact credentials to inject into the harness env.
136
+ * Delete a Hydra OAuth2 client if it exists, then create it at a known
137
+ * client_id (with a known secret, for a confidential client). We always
138
+ * recreate so the launcher knows the exact credentials to inject into the
139
+ * harness env.
140
+ *
141
+ * The client is addressed **by id**, never by `client_name`. That mirrors how
142
+ * Ory Network resolves the shared login client — the name is a display label
143
+ * anyone can register a client under, so only the reserved id identifies it
144
+ * (see `USER_LOGIN_CLIENT_ID`) — so local dev cannot pass with a lookup shape
145
+ * that would be unsafe against a hosted project.
129
146
  */
130
147
  async function ensureOAuth2Client(spec) {
131
- // Look up by client_name; delete if present so we can recreate with a
132
- // deterministic secret.
133
- const existing = (await jsonFetch(`${HYDRA_ADMIN}/admin/clients?client_name=${encodeURIComponent(spec.clientName)}`));
134
- for (const c of existing) {
135
- if (c.client_name !== spec.clientName)
136
- continue;
137
- try {
138
- await jsonFetch(`${HYDRA_ADMIN}/admin/clients/${encodeURIComponent(c.client_id)}`, {
139
- method: "DELETE",
140
- });
141
- }
142
- catch {
143
- // best-effort cleanup
144
- }
148
+ // Delete the client at this id if one is already there, so the recreate
149
+ // below lands with the exact grants, scopes, and redirect URIs this seed
150
+ // declares. A 404 (nothing to delete) is the normal first-run case.
151
+ try {
152
+ await jsonFetch(`${HYDRA_ADMIN}/admin/clients/${encodeURIComponent(spec.clientId)}`, {
153
+ method: "DELETE",
154
+ });
155
+ }
156
+ catch {
157
+ // best-effort cleanup — absent, or Hydra refused; the create below reports
158
+ // the real problem if there is one.
145
159
  }
146
160
  const body = {
147
161
  client_id: spec.clientId,
@@ -174,7 +188,7 @@ function loopbackRedirectUris() {
174
188
  * Returns a `SeedResult` describing both identities, the user's
175
189
  * pre-minted session, and the OAuth2 clients registered for each.
176
190
  */
177
- async function seedLocalEnvironment(namespace = "AgentTools") {
191
+ async function seedLocalEnvironment(namespace = "AgentTool") {
178
192
  process.stderr.write("Seeding local Ory environment...\n\n");
179
193
  // 0. Readiness barrier. The seed writes directly to Kratos (admin),
180
194
  // Keto (write), and Hydra (admin). Neither the static nginx gateway
@@ -203,6 +217,14 @@ async function seedLocalEnvironment(namespace = "AgentTools") {
203
217
  process.stderr.write(` Creating permissions for ${COMMON_TOOLS.length} tools...\n`);
204
218
  const tupleCount = await seedPermissions(userIdentity.id, namespace);
205
219
  process.stderr.write(` Permissions: ${tupleCount} entries in '${namespace}' for ${subjectLabel}\n`);
220
+ // Shell-command sub-tools (issue #76): grant every catalog word in the
221
+ // ShellTool namespace so a launched harness can run `bash -c "curl …"`
222
+ // out of the box, and the enforce demo (revoke `ShellTool:curl#users`,
223
+ // watch it block) works without hand-writing tuples first.
224
+ const shellWords = (0, tool_catalog_js_1.getShellCommandCatalog)();
225
+ const shellNamespace = (0, tool_catalog_js_1.resolveShellCommandNamespace)();
226
+ const shellCount = await seedPermissions(userIdentity.id, shellNamespace, shellWords);
227
+ process.stderr.write(` Shell commands: ${shellCount} entries in '${shellNamespace}' for ${subjectLabel}\n`);
206
228
  // 3. User OAuth2 client (PKCE) — pre-registered so the user gate's
207
229
  // PKCE flow has somewhere to authenticate against. The agent's
208
230
  // OAuth2 client is intentionally NOT pre-registered; the harness
@@ -211,13 +233,15 @@ async function seedLocalEnvironment(namespace = "AgentTools") {
211
233
  let userClient = { clientId: exports.USER_CLIENT_ID };
212
234
  try {
213
235
  userClient = await ensureOAuth2Client({
214
- clientName: USER_CLIENT_NAME,
236
+ // Display label only, matching what a hosted project shows in the
237
+ // Console — the client is addressed by its id.
238
+ clientName: branding_js_1.USER_LOGIN_CLIENT_NAME,
215
239
  clientId: exports.USER_CLIENT_ID,
216
240
  // Public PKCE client — no secret. Hydra accepts an empty string
217
241
  // for `token_endpoint_auth_method=none`.
218
242
  grantTypes: ["authorization_code", "refresh_token"],
219
243
  responseTypes: ["code"],
220
- scope: "openid offline_access",
244
+ scope: branding_js_1.USER_LOGIN_SCOPE_OAUTH,
221
245
  tokenEndpointAuthMethod: "none",
222
246
  redirectUris: loopbackRedirectUris(),
223
247
  });