@lamalibre/create-lamaste 4.4.1 → 4.6.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 (170) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +234 -18
  3. package/src/index.js +1 -0
  4. package/src/lib/backend-cookie.js +65 -0
  5. package/src/lib/ip-panel-vhost.js +208 -0
  6. package/src/lib/service-config.js +5 -3
  7. package/src/tasks/nginx.js +18 -135
  8. package/src/tasks/redeploy.js +39 -1
  9. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  10. package/vendor/docs/00-introduction/quickstart.md +5 -5
  11. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  12. package/vendor/docs/01-concepts/authentication.md +1 -1
  13. package/vendor/docs/01-concepts/authorization.md +66 -48
  14. package/vendor/docs/01-concepts/certificates.md +3 -3
  15. package/vendor/docs/01-concepts/mtls.md +2 -2
  16. package/vendor/docs/01-concepts/security-model.md +70 -33
  17. package/vendor/docs/01-concepts/tickets.md +2 -2
  18. package/vendor/docs/01-concepts/tunneling.md +4 -4
  19. package/vendor/docs/02-guides/agent-setup.md +5 -5
  20. package/vendor/docs/02-guides/certificate-management.md +2 -2
  21. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  22. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  23. package/vendor/docs/02-guides/installation.md +3 -3
  24. package/vendor/docs/02-guides/managing-users.md +113 -25
  25. package/vendor/docs/02-guides/onboarding.md +2 -2
  26. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  27. package/vendor/docs/02-guides/static-sites.md +4 -0
  28. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  29. package/vendor/docs/03-architecture/installer.md +4 -4
  30. package/vendor/docs/03-architecture/management-flow.md +3 -1
  31. package/vendor/docs/03-architecture/nginx-configuration.md +229 -24
  32. package/vendor/docs/03-architecture/overview.md +13 -17
  33. package/vendor/docs/03-architecture/panel-client.md +6 -6
  34. package/vendor/docs/03-architecture/panel-server.md +3 -2
  35. package/vendor/docs/03-architecture/state-management.md +4 -8
  36. package/vendor/docs/03-architecture/system-overview.md +1 -1
  37. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +378 -34
  38. package/vendor/docs/04-api-reference/app-host.md +3 -3
  39. package/vendor/docs/04-api-reference/gatekeeper.md +78 -5
  40. package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
  41. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  42. package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
  43. package/vendor/docs/04-api-reference/sites.md +4 -0
  44. package/vendor/docs/04-api-reference/tunnels.md +87 -47
  45. package/vendor/docs/04-api-reference/users.md +241 -45
  46. package/vendor/docs/05-operations/backup-and-restore.md +35 -26
  47. package/vendor/docs/05-operations/monitoring.md +39 -0
  48. package/vendor/docs/05-operations/uninstalling.md +2 -1
  49. package/vendor/docs/05-operations/upgrades.md +24 -6
  50. package/vendor/docs/06-reference/config-files.md +100 -202
  51. package/vendor/docs/06-reference/glossary.md +3 -3
  52. package/vendor/docs/06-reference/installer-flags.md +13 -13
  53. package/vendor/docs/06-reference/ports-and-services.md +25 -26
  54. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  55. package/vendor/docs/gatekeeper-design.md +10 -8
  56. package/vendor/docs/identity-directory-design.md +48 -14
  57. package/vendor/docs/package.json +1 -1
  58. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  59. package/vendor/gatekeeper/dist/cli/index.js +0 -0
  60. package/vendor/gatekeeper/dist/index.d.ts +4 -3
  61. package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
  62. package/vendor/gatekeeper/dist/index.js +4 -2
  63. package/vendor/gatekeeper/dist/index.js.map +1 -1
  64. package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
  65. package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
  66. package/vendor/gatekeeper/dist/lib/authz.js +31 -0
  67. package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
  68. package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
  69. package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
  70. package/vendor/gatekeeper/dist/lib/constants.js +5 -2
  71. package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
  72. package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
  73. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  74. package/vendor/gatekeeper/dist/lib/grants.js +6 -1
  75. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  76. package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
  77. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
  78. package/vendor/gatekeeper/dist/lib/groups.js +5 -1
  79. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
  80. package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
  81. package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
  82. package/vendor/gatekeeper/dist/lib/principals.js +67 -0
  83. package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
  84. package/vendor/gatekeeper/dist/lib/tunnels.d.ts +10 -0
  85. package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
  86. package/vendor/gatekeeper/dist/lib/tunnels.js +56 -0
  87. package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
  88. package/vendor/gatekeeper/dist/server/index.d.ts +2 -1
  89. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  90. package/vendor/gatekeeper/dist/server/index.js +49 -56
  91. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  92. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
  93. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  94. package/vendor/gatekeeper/dist/server/routes/authz.js +332 -104
  95. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  96. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  97. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
  98. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  99. package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
  100. package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
  101. package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
  102. package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
  103. package/vendor/gatekeeper/package.json +2 -2
  104. package/vendor/server/bin/lamaste-server.js +15 -1
  105. package/vendor/server/package.json +2 -2
  106. package/vendor/server/src/commands/plugin-identity.js +335 -0
  107. package/vendor/server/src/commands/plugin-web-surface.js +269 -0
  108. package/vendor/server/src/commands/plugins.js +156 -11
  109. package/vendor/server/src/commands/registry-credential.js +2 -1
  110. package/vendor/server/src/commands/sites.js +15 -1
  111. package/vendor/server/src/commands/tunnels.js +102 -2
  112. package/vendor/server/src/commands/uninstall.js +3 -1
  113. package/vendor/server/src/commands/users.js +358 -0
  114. package/vendor/server/src/plugin-approval.js +186 -5
  115. package/vendor/server/src/plugin-identity.js +382 -0
  116. package/vendor/server/src/plugin-web-surface.js +307 -0
  117. package/vendor/server-ui/dist/assets/index-BZwQ_aQc.css +1 -0
  118. package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +84 -0
  119. package/vendor/server-ui/dist/index.html +2 -2
  120. package/vendor/serverd/package.json +2 -2
  121. package/vendor/serverd/src/index.js +34 -5
  122. package/vendor/serverd/src/lib/audience.js +576 -0
  123. package/vendor/serverd/src/lib/authelia.js +78 -7
  124. package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
  125. package/vendor/serverd/src/lib/chisel.js +10 -2
  126. package/vendor/serverd/src/lib/config.js +68 -43
  127. package/vendor/serverd/src/lib/directory.js +1113 -0
  128. package/vendor/serverd/src/lib/entitlements.js +517 -0
  129. package/vendor/serverd/src/lib/gatekeeper-client.js +226 -10
  130. package/vendor/serverd/src/lib/hook-pipelines.js +21 -3
  131. package/vendor/serverd/src/lib/identity-sync.js +905 -0
  132. package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
  133. package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
  134. package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
  135. package/vendor/serverd/src/lib/mtls.js +6 -2
  136. package/vendor/serverd/src/lib/nginx.js +546 -48
  137. package/vendor/serverd/src/lib/plugin-hooks.js +127 -45
  138. package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
  139. package/vendor/serverd/src/lib/plugin-lifecycle.js +227 -13
  140. package/vendor/serverd/src/lib/plugin-packages.js +74 -2
  141. package/vendor/serverd/src/lib/plugins.js +95 -7
  142. package/vendor/serverd/src/lib/registry-credential.js +9 -6
  143. package/vendor/serverd/src/lib/request-logging.js +19 -0
  144. package/vendor/serverd/src/lib/retained-data.js +50 -4
  145. package/vendor/serverd/src/lib/state.js +36 -0
  146. package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
  147. package/vendor/serverd/src/lib/user-access-session.js +21 -11
  148. package/vendor/serverd/src/lib/user-caller.js +67 -0
  149. package/vendor/serverd/src/lib/user-surface.js +754 -0
  150. package/vendor/serverd/src/lib/vhost-reconcile.js +137 -33
  151. package/vendor/serverd/src/lib/web-surface.js +766 -0
  152. package/vendor/serverd/src/middleware/user-access-session.js +9 -0
  153. package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
  154. package/vendor/serverd/src/routes/invite.js +53 -12
  155. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
  156. package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
  157. package/vendor/serverd/src/routes/management/invitations.js +78 -68
  158. package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
  159. package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
  160. package/vendor/serverd/src/routes/management/plugins.js +2 -0
  161. package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
  162. package/vendor/serverd/src/routes/management/sites.js +143 -65
  163. package/vendor/serverd/src/routes/management/tunnels.js +109 -22
  164. package/vendor/serverd/src/routes/management/users.js +290 -208
  165. package/vendor/serverd/src/routes/management.js +6 -0
  166. package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
  167. package/vendor/serverd/src/routes/user-access.js +17 -4
  168. package/vendor/serverd/src/routes/user-surface.js +69 -0
  169. package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
  170. package/vendor/server-ui/dist/assets/index-roDHz3dM.js +0 -84
@@ -0,0 +1,221 @@
1
+ /**
2
+ * A plugin's users (slice 6): what its identity block declares, who is
3
+ * entitled to it with which roles, its audience as the agent half is told,
4
+ * and the feed that tells it. Admin only. Business logic lives in
5
+ * `lib/entitlements.js`, `lib/audience.js` and `lib/identity-sync.js`.
6
+ *
7
+ * GET /plugins/:name/identity declared block, approval, readiness,
8
+ * sync state, audience counts, pending erasures
9
+ * GET /plugins/:name/audience members and tombstones, in seq order
10
+ * GET /plugins/:name/entitlements role grants (and how many grants carry no roles)
11
+ * PUT /plugins/:name/entitlements { principalType, principalId, roles }
12
+ * DELETE /plugins/:name/entitlements?principalType=&principalId=
13
+ * POST /plugins/:name/identity/sync { reconcile? } — sync now
14
+ */
15
+
16
+ import { z } from 'zod';
17
+ import { USERNAME_MAX_LENGTH, USERNAME_MIN_LENGTH, USERNAME_REGEX } from '@lamalibre/lamaste';
18
+ import { actorFromRequest } from '../../lib/actor.js';
19
+ import { listAudience } from '../../lib/audience.js';
20
+ import { listEntitlements, removeEntitlement, setEntitlement } from '../../lib/entitlements.js';
21
+ import { identityView, syncPluginIdentity } from '../../lib/identity-sync.js';
22
+ import { hookStatus } from '../../lib/plugin-hooks.js';
23
+ import { findPlugin } from '../../lib/plugins.js';
24
+
25
+ const PluginNameParamSchema = z.object({
26
+ name: z
27
+ .string()
28
+ .min(1)
29
+ .max(100)
30
+ .regex(/^[a-z0-9-]+$/, 'Plugin name must contain only lowercase letters, numbers, and hyphens'),
31
+ });
32
+
33
+ /** A user principal is a username, a group principal a Lamaste group name (Gatekeeper's rule). */
34
+ const USER_PRINCIPAL = {
35
+ principalType: z.literal('user'),
36
+ principalId: z
37
+ .string()
38
+ .min(USERNAME_MIN_LENGTH)
39
+ .max(USERNAME_MAX_LENGTH)
40
+ .regex(USERNAME_REGEX, 'principalId is a username'),
41
+ };
42
+ const GROUP_PRINCIPAL = {
43
+ principalType: z.literal('group'),
44
+ principalId: z
45
+ .string()
46
+ .min(2)
47
+ .max(63)
48
+ .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, 'principalId is a group name'),
49
+ };
50
+ /** Role ids are checked against the manifest by the library. */
51
+ const ROLES = z.array(z.string().min(1).max(32)).min(1).max(32);
52
+
53
+ const PrincipalSchema = z.discriminatedUnion('principalType', [
54
+ z.object(USER_PRINCIPAL).strict(),
55
+ z.object(GROUP_PRINCIPAL).strict(),
56
+ ]);
57
+
58
+ const EntitlementBodySchema = z.discriminatedUnion('principalType', [
59
+ z.object({ ...USER_PRINCIPAL, roles: ROLES }).strict(),
60
+ z.object({ ...GROUP_PRINCIPAL, roles: ROLES }).strict(),
61
+ ]);
62
+
63
+ const SyncBodySchema = z.object({ reconcile: z.boolean().optional() }).strict();
64
+
65
+ /** The refusal a library threw (its `statusCode`, message and `code`), or a 500. */
66
+ function sendFailure(request, reply, err, fallback) {
67
+ if (Number.isInteger(err.statusCode)) {
68
+ return reply.code(err.statusCode).send({
69
+ error: err.message,
70
+ ...(err.code ? { code: err.code } : {}),
71
+ ...(Array.isArray(err.unknown) ? { unknown: err.unknown } : {}),
72
+ });
73
+ }
74
+ request.log.error(err, fallback);
75
+ return reply.code(500).send({ error: fallback });
76
+ }
77
+
78
+ /** The installed plugin, answering 404 (not installed) or 400 `no_identity` (when it must declare identity). */
79
+ async function pluginFor(request, reply, { requireIdentity }) {
80
+ const { name } = PluginNameParamSchema.parse(request.params);
81
+ const plugin = await findPlugin(name);
82
+ if (!plugin) {
83
+ reply.code(404).send({ error: `Plugin "${name}" not found` });
84
+ return null;
85
+ }
86
+ if (requireIdentity && !plugin.identity) {
87
+ reply
88
+ .code(400)
89
+ .send({ error: `Plugin "${name}" declares no identity (roles)`, code: 'no_identity' });
90
+ return null;
91
+ }
92
+ return plugin;
93
+ }
94
+
95
+ export default async function pluginIdentityRoutes(fastify, _opts) {
96
+ fastify.get(
97
+ '/plugins/:name/identity',
98
+ { preHandler: fastify.requireRole(['admin']) },
99
+ async (request, reply) => {
100
+ const plugin = await pluginFor(request, reply, { requireIdentity: false });
101
+ if (!plugin) return reply;
102
+ const [view, hooks] = await Promise.all([identityView(plugin.name), hookStatus(plugin)]);
103
+ return {
104
+ identity: {
105
+ ...view,
106
+ approval: { approved: hooks.identity.approved, state: hooks.approval.state },
107
+ },
108
+ };
109
+ },
110
+ );
111
+
112
+ fastify.get(
113
+ '/plugins/:name/audience',
114
+ { preHandler: fastify.requireRole(['admin']) },
115
+ async (request, reply) => {
116
+ const plugin = await pluginFor(request, reply, { requireIdentity: true });
117
+ if (!plugin) return reply;
118
+ const rows = await listAudience(plugin.name);
119
+ return {
120
+ head: rows.reduce((head, row) => Math.max(head, row.seq), 0),
121
+ audience: rows.map((row) => ({
122
+ userId: row.userId,
123
+ seq: row.seq,
124
+ version: row.version,
125
+ updatedAt: row.updatedAt,
126
+ change: row.change,
127
+ })),
128
+ };
129
+ },
130
+ );
131
+
132
+ fastify.get(
133
+ '/plugins/:name/entitlements',
134
+ { preHandler: fastify.requireRole(['admin']) },
135
+ async (request, reply) => {
136
+ const plugin = await pluginFor(request, reply, { requireIdentity: true });
137
+ if (!plugin) return reply;
138
+ try {
139
+ return await listEntitlements(plugin);
140
+ } catch (err) {
141
+ return sendFailure(request, reply, err, 'Failed to read the entitlements');
142
+ }
143
+ },
144
+ );
145
+
146
+ fastify.put(
147
+ '/plugins/:name/entitlements',
148
+ {
149
+ preHandler: fastify.requireRole(['admin']),
150
+ config: { rateLimit: { max: 30, timeWindow: '1 minute' } },
151
+ },
152
+ async (request, reply) => {
153
+ const plugin = await pluginFor(request, reply, { requireIdentity: true });
154
+ if (!plugin) return reply;
155
+ const body = EntitlementBodySchema.parse(request.body);
156
+ try {
157
+ const result = await setEntitlement(plugin, body, {
158
+ actor: actorFromRequest(request),
159
+ logger: request.log,
160
+ });
161
+ return {
162
+ ok: true,
163
+ entitlement: result.entitlement,
164
+ changed: result.changed,
165
+ ...(result.warnings.length > 0 ? { warnings: result.warnings } : {}),
166
+ };
167
+ } catch (err) {
168
+ return sendFailure(request, reply, err, 'Failed to set the entitlement');
169
+ }
170
+ },
171
+ );
172
+
173
+ fastify.delete(
174
+ '/plugins/:name/entitlements',
175
+ {
176
+ preHandler: fastify.requireRole(['admin']),
177
+ config: { rateLimit: { max: 30, timeWindow: '1 minute' } },
178
+ },
179
+ async (request, reply) => {
180
+ const plugin = await pluginFor(request, reply, { requireIdentity: true });
181
+ if (!plugin) return reply;
182
+ const principal = PrincipalSchema.parse(request.query ?? {});
183
+ try {
184
+ const result = await removeEntitlement(plugin, principal, {
185
+ actor: actorFromRequest(request),
186
+ logger: request.log,
187
+ });
188
+ return {
189
+ ok: true,
190
+ revoked: result.revoked,
191
+ ...(result.warnings.length > 0 ? { warnings: result.warnings } : {}),
192
+ };
193
+ } catch (err) {
194
+ return sendFailure(request, reply, err, 'Failed to remove the entitlement');
195
+ }
196
+ },
197
+ );
198
+
199
+ fastify.post(
200
+ '/plugins/:name/identity/sync',
201
+ {
202
+ preHandler: fastify.requireRole(['admin']),
203
+ config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
204
+ },
205
+ async (request, reply) => {
206
+ const plugin = await pluginFor(request, reply, { requireIdentity: true });
207
+ if (!plugin) return reply;
208
+ const body = SyncBodySchema.parse(request.body ?? {});
209
+ const result = await syncPluginIdentity(plugin.name, {
210
+ reconcile: body.reconcile === true,
211
+ logger: request.log,
212
+ });
213
+ const view = await identityView(plugin.name);
214
+ return {
215
+ ok: result.status === 'synced' || result.status === 'up-to-date',
216
+ result,
217
+ sync: view?.sync ?? null,
218
+ };
219
+ },
220
+ );
221
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * A plugin's web surface (slice 7): the site it owns, where the relay
3
+ * serves the shell its server package ships beside its user surface's API.
4
+ * Admin only. Business logic lives in `lib/web-surface.js`.
5
+ *
6
+ * GET /plugins/:name/web-surface what the installed version declares, the
7
+ * site, whether it is served, the trees, the offer
8
+ * POST /plugins/:name/web-surface { subdomain?, accessMode? } — create the site
9
+ * POST /plugins/:name/web-surface/move { tunnelId } — take over the bound tunnel's host
10
+ * DELETE /plugins/:name/web-surface delete the site and its trees
11
+ */
12
+
13
+ import { z } from 'zod';
14
+ import { WEB_SURFACE_ACCESS_MODES, derivePluginRoute } from '@lamalibre/lamaste';
15
+ import { actorFromRequest } from '../../lib/actor.js';
16
+ import { findPlugin } from '../../lib/plugins.js';
17
+ import {
18
+ createWebSurface,
19
+ deleteWebSurface,
20
+ moveWebSurfaceFromTunnel,
21
+ webSurfaceStatus,
22
+ } from '../../lib/web-surface.js';
23
+
24
+ const PluginNameParamSchema = z.object({
25
+ name: z
26
+ .string()
27
+ .min(1)
28
+ .max(100)
29
+ .regex(/^[a-z0-9-]+$/, 'Plugin name must contain only lowercase letters, numbers, and hyphens'),
30
+ });
31
+
32
+ const CreateBodySchema = z
33
+ .object({
34
+ subdomain: z
35
+ .string()
36
+ .min(1)
37
+ .max(63)
38
+ .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, 'subdomain is a DNS label')
39
+ .optional(),
40
+ accessMode: z.enum(WEB_SURFACE_ACCESS_MODES).optional(),
41
+ })
42
+ .strict();
43
+
44
+ const MoveBodySchema = z.object({ tunnelId: z.string().min(1).max(64) }).strict();
45
+
46
+ /** The refusal a library threw (its `statusCode`, message and `code`), or a 500. */
47
+ function sendFailure(request, reply, err, fallback) {
48
+ if (Number.isInteger(err.statusCode) && err.statusCode < 500) {
49
+ return reply
50
+ .code(err.statusCode)
51
+ .send({ error: err.message, ...(err.code ? { code: err.code } : {}) });
52
+ }
53
+ request.log.error(err, fallback);
54
+ return reply
55
+ .code(Number.isInteger(err.statusCode) ? err.statusCode : 500)
56
+ .send({ error: fallback, details: err.message });
57
+ }
58
+
59
+ /**
60
+ * The route plugin `:name` holds: the installed plugin's, else the one its
61
+ * name derives — a site outlives its plugin, and its status and deletion
62
+ * stay reachable (creating or moving refuses without an installed plugin).
63
+ */
64
+ async function routeOf(request) {
65
+ const { name } = PluginNameParamSchema.parse(request.params);
66
+ const plugin = await findPlugin(name);
67
+ return plugin?.route ?? derivePluginRoute(name);
68
+ }
69
+
70
+ export default async function pluginWebSurfaceRoutes(fastify, _opts) {
71
+ fastify.get(
72
+ '/plugins/:name/web-surface',
73
+ { preHandler: fastify.requireRole(['admin']) },
74
+ async (request, reply) => {
75
+ const route = await routeOf(request);
76
+ try {
77
+ return { webSurface: await webSurfaceStatus(route) };
78
+ } catch (err) {
79
+ return sendFailure(request, reply, err, 'Failed to read the web surface');
80
+ }
81
+ },
82
+ );
83
+
84
+ fastify.post(
85
+ '/plugins/:name/web-surface',
86
+ {
87
+ preHandler: fastify.requireRole(['admin']),
88
+ // Issues a certificate and reloads nginx.
89
+ config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
90
+ },
91
+ async (request, reply) => {
92
+ const route = await routeOf(request);
93
+ const body = CreateBodySchema.parse(request.body ?? {});
94
+ try {
95
+ const webSurface = await createWebSurface(route, body, {
96
+ actor: actorFromRequest(request),
97
+ logger: request.log,
98
+ });
99
+ return reply.code(201).send({ webSurface });
100
+ } catch (err) {
101
+ return sendFailure(request, reply, err, 'Failed to create the web surface');
102
+ }
103
+ },
104
+ );
105
+
106
+ fastify.post(
107
+ '/plugins/:name/web-surface/move',
108
+ {
109
+ preHandler: fastify.requireRole(['admin']),
110
+ config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
111
+ },
112
+ async (request, reply) => {
113
+ const route = await routeOf(request);
114
+ const { tunnelId } = MoveBodySchema.parse(request.body);
115
+ try {
116
+ return {
117
+ webSurface: await moveWebSurfaceFromTunnel(route, tunnelId, {
118
+ actor: actorFromRequest(request),
119
+ logger: request.log,
120
+ }),
121
+ };
122
+ } catch (err) {
123
+ return sendFailure(
124
+ request,
125
+ reply,
126
+ err,
127
+ 'Failed to move the user surface to the web surface',
128
+ );
129
+ }
130
+ },
131
+ );
132
+
133
+ fastify.delete(
134
+ '/plugins/:name/web-surface',
135
+ {
136
+ preHandler: fastify.requireRole(['admin']),
137
+ config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
138
+ },
139
+ async (request, reply) => {
140
+ const route = await routeOf(request);
141
+ try {
142
+ return await deleteWebSurface(route, {
143
+ actor: actorFromRequest(request),
144
+ logger: request.log,
145
+ });
146
+ } catch (err) {
147
+ return sendFailure(request, reply, err, 'Failed to delete the web surface');
148
+ }
149
+ },
150
+ );
151
+ }
@@ -301,6 +301,8 @@ async function withHooks(plugin) {
301
301
  agent: status.agent !== null,
302
302
  // Lamaste installs the agent half (agent.package); false: none, or installed by hand.
303
303
  agentHalf: status.agentHalf !== null,
304
+ // The installed version declares a web surface (its tree is in the approval).
305
+ web: status.web.declared,
304
306
  approval: status.approval.state,
305
307
  autoApprove: status.autoApprove,
306
308
  failOpenCount: status.relay?.failOpenCount ?? 0,
@@ -4,7 +4,7 @@
4
4
  * GET /api/registry-credential what is configured — never a token
5
5
  * PUT /api/registry-credential store a credential after verifying the
6
6
  * whole chain against the registry
7
- * (admin scope required)
7
+ * (read + admin scopes required)
8
8
  * DELETE /api/registry-credential remove it
9
9
  *
10
10
  * `auth.kind` is `token` (a pasted long-lived Feria token) or `oidc` (the