@camstack/server 1.2.605 → 1.2.607
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/api/auth-whoami.js +0 -1
- package/dist/api/core/auth-audit.procedures.js +44 -0
- package/dist/api/core/auth-revoke-credentials.procedures.js +4 -78
- package/dist/api/core/auth-tokens.procedures.js +105 -72
- package/dist/api/core/auth.router.js +14 -280
- package/dist/api/core/cap-providers.js +38 -1
- package/dist/api/core/integration-secrets.js +22 -1
- package/dist/api/core/logs.router.js +219 -49
- package/dist/api/core/preferences.router.js +98 -0
- package/dist/api/oauth2/oauth2-routes.js +40 -15
- package/dist/api/trpc/device-config-secret-redaction.js +2 -2
- package/dist/api/trpc/generated-cap-mounts.js +0 -1
- package/dist/api/trpc/generated-cap-routers.js +53 -42
- package/dist/api/trpc/principal-visibility.js +2 -2
- package/dist/api/trpc/share-view-access.js +1 -1
- package/dist/api/trpc/trpc-error-principal.js +1 -2
- package/dist/api/trpc/trpc.context.js +16 -13
- package/dist/api/trpc/trpc.router.js +7 -2
- package/dist/api/upload-auth.js +13 -25
- package/dist/auth/addon-route-share-gate.js +2 -2
- package/dist/auth/credential-verifier.js +63 -104
- package/dist/auth/embed-auth-route.js +45 -0
- package/dist/auth/live-user.js +19 -0
- package/dist/auth/scope-escalation.js +77 -2
- package/dist/boot/boot-config.js +8 -1
- package/dist/core/addon/addon-registry.service.js +14 -20
- package/dist/core/auth/credential-token.service.js +9 -14
- package/dist/core/auth/handoff-code.service.js +1 -1
- package/dist/core/auth/record-kinds.js +7 -2
- package/dist/core/logging/log-history.js +119 -0
- package/dist/core/logging/logging.service.js +38 -1
- package/dist/main.js +18 -48
- package/package.json +14 -14
- package/dist/api/core/auth-admin-tokens.procedures.js +0 -156
- package/dist/core/auth/share-token.service.js +0 -459
|
@@ -32,11 +32,9 @@ const session_cookie_js_1 = require("../../auth/session-cookie.js");
|
|
|
32
32
|
const hub_sessions_js_1 = require("../../auth/hub-sessions.js");
|
|
33
33
|
const session_mint_js_1 = require("../../auth/session-mint.js");
|
|
34
34
|
const handoff_code_service_js_1 = require("../../core/auth/handoff-code.service.js");
|
|
35
|
-
const share_token_service_js_1 = require("../../core/auth/share-token.service.js");
|
|
36
|
-
const principal_visibility_js_1 = require("../trpc/principal-visibility.js");
|
|
37
35
|
const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
|
|
38
36
|
const auth_limits_js_1 = require("./auth-limits.js");
|
|
39
|
-
const
|
|
37
|
+
const auth_audit_procedures_js_1 = require("./auth-audit.procedures.js");
|
|
40
38
|
const auth_tokens_procedures_js_1 = require("./auth-tokens.procedures.js");
|
|
41
39
|
const auth_revoke_credentials_procedures_js_1 = require("./auth-revoke-credentials.procedures.js");
|
|
42
40
|
const auth_reauth_procedures_js_1 = require("./auth-reauth.procedures.js");
|
|
@@ -170,59 +168,14 @@ const OwnExternalIdentitySchema = zod_1.z.object({
|
|
|
170
168
|
/** Whether the provider instance is configured and enabled right now. */
|
|
171
169
|
available: zod_1.z.boolean(),
|
|
172
170
|
});
|
|
173
|
-
// ── Share tokens (grid-view standalone share links) ──────────────────
|
|
174
|
-
//
|
|
175
|
-
// Wire schemas for the `csv_*` share-token surface. The summary shape
|
|
176
|
-
// deliberately omits `tokenHash` — the raw token is returned exactly
|
|
177
|
-
// once (at mint); afterwards only prefix + scope + timestamps are
|
|
178
|
-
// visible.
|
|
179
|
-
//
|
|
180
|
-
// TTL: a bounded number of seconds, or the EXPLICIT literal `'never'`
|
|
181
|
-
// for a permanently-shared grid (revocation is the kill switch).
|
|
182
|
-
// Omitted → 7-day default; infinite is never a silent default.
|
|
183
|
-
const CreateShareTokenInputSchema = zod_1.z.object({
|
|
184
|
-
scope: share_token_service_js_1.ShareTokenScopeSchema,
|
|
185
|
-
ttlSec: zod_1.z
|
|
186
|
-
.union([
|
|
187
|
-
zod_1.z.number().int().min(share_token_service_js_1.SHARE_TOKEN_TTL_MIN_SEC).max(share_token_service_js_1.SHARE_TOKEN_TTL_MAX_SEC),
|
|
188
|
-
zod_1.z.literal('never'),
|
|
189
|
-
])
|
|
190
|
-
.optional(),
|
|
191
|
-
});
|
|
192
|
-
const ShareTokenSummarySchema = zod_1.z.object({
|
|
193
|
-
id: zod_1.z.string(),
|
|
194
|
-
userId: zod_1.z.string(),
|
|
195
|
-
tokenPrefix: zod_1.z.string(),
|
|
196
|
-
scope: share_token_service_js_1.ShareTokenScopeSchema,
|
|
197
|
-
createdAt: zod_1.z.number(),
|
|
198
|
-
/** Epoch ms, or `null` for never-expiring tokens. */
|
|
199
|
-
expiresAt: zod_1.z.number().nullable(),
|
|
200
|
-
/** Derived convenience flag so listings surface infinite tokens clearly. */
|
|
201
|
-
neverExpires: zod_1.z.boolean(),
|
|
202
|
-
lastUsedAt: zod_1.z.number().optional(),
|
|
203
|
-
/** A pre-D892 `csv_` link: valid until it expires, never minted again. */
|
|
204
|
-
legacy: zod_1.z.boolean().optional(),
|
|
205
|
-
});
|
|
206
|
-
function toShareTokenSummary(record) {
|
|
207
|
-
return {
|
|
208
|
-
id: record.id,
|
|
209
|
-
userId: record.userId,
|
|
210
|
-
tokenPrefix: record.tokenPrefix,
|
|
211
|
-
scope: record.scope,
|
|
212
|
-
createdAt: record.createdAt,
|
|
213
|
-
expiresAt: record.expiresAt,
|
|
214
|
-
neverExpires: record.expiresAt === null,
|
|
215
|
-
...(record.lastUsedAt !== undefined ? { lastUsedAt: record.lastUsedAt } : {}),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
171
|
/**
|
|
219
172
|
* Token minting/management surfaces are for REAL user sessions only.
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
173
|
+
* A minted token — whatever its scopes — must never mint handoff codes or
|
|
174
|
+
* link sign-in accounts: a leaked restricted token would otherwise widen its
|
|
175
|
+
* own reach.
|
|
223
176
|
*/
|
|
224
|
-
function assertRealUserSession(user, what
|
|
225
|
-
// A minted token (admin, scoped
|
|
177
|
+
function assertRealUserSession(user, what) {
|
|
178
|
+
// A minted token (admin, scoped or share-view) is a script's
|
|
226
179
|
// credential, never a person's: it must not turn itself into a session
|
|
227
180
|
// (handoff codes) or mint links. Provenance, not permission (D892).
|
|
228
181
|
if (user.userDelegated !== true) {
|
|
@@ -301,7 +254,7 @@ const MeSchema = zod_1.z
|
|
|
301
254
|
scope: EffectiveScopeSchema.optional(),
|
|
302
255
|
})
|
|
303
256
|
.nullable();
|
|
304
|
-
function createAuthRouter(auth, registry, moleculer = null,
|
|
257
|
+
function createAuthRouter(auth, registry, moleculer = null, handoffCodes = new handoff_code_service_js_1.HandoffCodeService(), linkIntents = new external_link_intents_js_1.ExternalLinkIntents(), nativeLink = null, options = {}) {
|
|
305
258
|
const network = options.network ?? auth_network_policy_js_1.DEFAULT_AUTH_NETWORK_POLICY;
|
|
306
259
|
const limiters = options.limiters ?? auth_limits_js_1.PROCESS_AUTH_LIMITERS;
|
|
307
260
|
/** Secret-verifying legs (password / TOTP / passkey assertion / codes). */
|
|
@@ -309,20 +262,6 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
309
262
|
/** Challenge-minting begin-legs — looser, they verify nothing. */
|
|
310
263
|
const ceremonyProcedure = makeRateLimitedProcedure(limiters.ceremony, network);
|
|
311
264
|
const credentialTokens = options.credentialTokens ?? null;
|
|
312
|
-
const tokenDeps = { auth, registry, network, credentialTokens, shareTokens };
|
|
313
|
-
/** The unified share link behind `id`, or `null` (a legacy `csv_`, or none). */
|
|
314
|
-
const unifiedShare = async (id) => {
|
|
315
|
-
if (credentialTokens === null)
|
|
316
|
-
return null;
|
|
317
|
-
const record = await credentialTokens.get(id);
|
|
318
|
-
return record !== null && (0, types_2.shareViewGrantOf)(record.scopes) !== null ? record : null;
|
|
319
|
-
};
|
|
320
|
-
const requireShareTokens = () => {
|
|
321
|
-
if (!shareTokens) {
|
|
322
|
-
throw new Error('Share tokens unavailable — service not wired on this node');
|
|
323
|
-
}
|
|
324
|
-
return shareTokens;
|
|
325
|
-
};
|
|
326
265
|
/**
|
|
327
266
|
* THE session mint (`auth/session-mint.ts`, D712) — the single shared tail
|
|
328
267
|
* of every successful login leg, including `/api/auth/sso/finish`. It
|
|
@@ -800,8 +739,8 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
800
739
|
* semantics (see `buildSessionCookie` — Lax already withholds
|
|
801
740
|
* the cookie from cross-site POSTs).
|
|
802
741
|
* • Only REAL session JWTs qualify: bridge tokens (`kind:
|
|
803
|
-
* 'totp-challenge'` / `'sso-bridge'`) and
|
|
804
|
-
*
|
|
742
|
+
* 'totp-challenge'` / `'sso-bridge'`) and minted tokens are
|
|
743
|
+
* rejected — the cookie must carry a v2 session.
|
|
805
744
|
* • The returned bearer is minted FRESH through the shared
|
|
806
745
|
* `mintSessionForUserId` tail (re-fetched user → up-to-date
|
|
807
746
|
* scopes), equivalent to what `auth.login` hands out.
|
|
@@ -851,8 +790,8 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
851
790
|
// for a real session. TTL 60s, single-use, bound to the minting
|
|
852
791
|
// user — see `HandoffCodeService`.
|
|
853
792
|
/** Mint a one-time handoff code for the CALLING user. Real user
|
|
854
|
-
* sessions only —
|
|
855
|
-
* must never convert
|
|
793
|
+
* sessions only — a minted token (scoped, admin or share-view)
|
|
794
|
+
* must never convert itself into a full session. */
|
|
856
795
|
createHandoffCode: trpc_middleware_js_1.protectedProcedure
|
|
857
796
|
.input(zod_1.z
|
|
858
797
|
.object({
|
|
@@ -1191,211 +1130,6 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1191
1130
|
enabled: input.enabled,
|
|
1192
1131
|
});
|
|
1193
1132
|
}),
|
|
1194
|
-
// ── Share tokens — scoped, TTL'd view tokens for standalone share
|
|
1195
|
-
// links (grid rework R6). Minted by any REAL user session (admin
|
|
1196
|
-
// or regular); the resulting `csv_*` token authenticates as a
|
|
1197
|
-
// restricted `share-view` principal limited to the embed surface
|
|
1198
|
-
// + its in-scope devices (see `share-view-access.ts`). ──────────
|
|
1199
|
-
createShareToken: trpc_middleware_js_1.protectedProcedure
|
|
1200
|
-
.input(CreateShareTokenInputSchema)
|
|
1201
|
-
.output(zod_1.z.object({ id: zod_1.z.string(), token: zod_1.z.string(), expiresAt: zod_1.z.number().nullable() }))
|
|
1202
|
-
.mutation(async ({ input, ctx }) => {
|
|
1203
|
-
assertRealUserSession(ctx.user);
|
|
1204
|
-
// SECURITY (F0.4): a share token is a PRIVILEGE HANDOFF — it authorises
|
|
1205
|
-
// whoever holds the link to view its devices with no further auth. A
|
|
1206
|
-
// scoped (non-admin) minter must therefore not be able to mint for a
|
|
1207
|
-
// device it cannot itself see: without this, a `device:[5]` token could
|
|
1208
|
-
// mint a `csv_*` grid link for cameras 5..99. Admins mint anything;
|
|
1209
|
-
// every other caller's requested deviceIds are INTERSECTED with what
|
|
1210
|
-
// they can view (device:[parent] covers child accessories via the
|
|
1211
|
-
// ancestor walk). An empty intersection is a refusal, not a silent
|
|
1212
|
-
// "share nothing".
|
|
1213
|
-
const scope = ctx.user.isAdmin
|
|
1214
|
-
? input.scope
|
|
1215
|
-
: {
|
|
1216
|
-
...input.scope,
|
|
1217
|
-
deviceIds: (0, principal_visibility_js_1.filterViewableDeviceIds)((0, types_2.capabilityScopesOf)(ctx.user.scopes ?? []), input.scope.deviceIds, ctx.deviceScopeLookup),
|
|
1218
|
-
};
|
|
1219
|
-
if (scope.deviceIds.length === 0) {
|
|
1220
|
-
throw new server_1.TRPCError({
|
|
1221
|
-
code: 'FORBIDDEN',
|
|
1222
|
-
message: 'None of the requested devices are within your scope — you cannot mint a share token for devices you cannot see',
|
|
1223
|
-
});
|
|
1224
|
-
}
|
|
1225
|
-
// A thin wrapper for one release (D892): the link is a unified token
|
|
1226
|
-
// whose only scope is the share-view grant. Without the store (a
|
|
1227
|
-
// node that cannot mint) it is still a legacy `csv_`.
|
|
1228
|
-
if (credentialTokens !== null) {
|
|
1229
|
-
const ttl = input.ttlSec ?? share_token_service_js_1.SHARE_TOKEN_TTL_DEFAULT_SEC;
|
|
1230
|
-
const minted = await (0, auth_tokens_procedures_js_1.mintFor)(tokenDeps, ctx, {
|
|
1231
|
-
scopes: [{ type: 'share-view', grant: scope }],
|
|
1232
|
-
expiresIn: ttl,
|
|
1233
|
-
label: `share ${scope.kind}`,
|
|
1234
|
-
allowAccountLink: true,
|
|
1235
|
-
});
|
|
1236
|
-
return { id: minted.record.id, token: minted.token, expiresAt: minted.record.expiresAt };
|
|
1237
|
-
}
|
|
1238
|
-
const { token, record } = await requireShareTokens().create({
|
|
1239
|
-
userId: ctx.user.id,
|
|
1240
|
-
scope,
|
|
1241
|
-
...(input.ttlSec !== undefined ? { ttlSec: input.ttlSec } : {}),
|
|
1242
|
-
});
|
|
1243
|
-
return { id: record.id, token, expiresAt: record.expiresAt };
|
|
1244
|
-
}),
|
|
1245
|
-
/**
|
|
1246
|
-
* Slide expiry on a still-valid share token (same `csv_*`). Used by the
|
|
1247
|
-
* Home Assistant embed cache so a long-lived dashboard does not mint a
|
|
1248
|
-
* new row every hour. Expired / missing → NOT_FOUND (caller must create).
|
|
1249
|
-
*/
|
|
1250
|
-
extendShareToken: trpc_middleware_js_1.protectedProcedure
|
|
1251
|
-
.input(zod_1.z.object({
|
|
1252
|
-
id: zod_1.z.string().min(1),
|
|
1253
|
-
ttlSec: zod_1.z.number().int().min(share_token_service_js_1.SHARE_TOKEN_TTL_MIN_SEC).max(share_token_service_js_1.SHARE_TOKEN_TTL_MAX_SEC),
|
|
1254
|
-
}))
|
|
1255
|
-
.output(zod_1.z.object({ id: zod_1.z.string(), expiresAt: zod_1.z.number() }))
|
|
1256
|
-
.mutation(async ({ input, ctx }) => {
|
|
1257
|
-
assertRealUserSession(ctx.user);
|
|
1258
|
-
const unified = await unifiedShare(input.id);
|
|
1259
|
-
if (unified !== null && credentialTokens !== null) {
|
|
1260
|
-
if (!ctx.user.isAdmin && unified.subjectId !== ctx.user.id) {
|
|
1261
|
-
throw new server_1.TRPCError({
|
|
1262
|
-
code: 'FORBIDDEN',
|
|
1263
|
-
message: 'Only the token owner or an admin can extend a share token',
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
if (unified.expiresAt === null) {
|
|
1267
|
-
throw (0, user_error_js_1.trpcUserError)('BAD_REQUEST', 'auth.share-never-expiring-extend', undefined, 'A never-expiring share token cannot be extended');
|
|
1268
|
-
}
|
|
1269
|
-
const moved = await credentialTokens.setExpiry(input.id, Date.now() + input.ttlSec * 1000);
|
|
1270
|
-
if (moved === null || moved.expiresAt === null) {
|
|
1271
|
-
throw new server_1.TRPCError({ code: 'NOT_FOUND', message: `Share token expired: ${input.id}` });
|
|
1272
|
-
}
|
|
1273
|
-
return { id: moved.id, expiresAt: moved.expiresAt };
|
|
1274
|
-
}
|
|
1275
|
-
const service = requireShareTokens();
|
|
1276
|
-
try {
|
|
1277
|
-
const record = await service.extend({
|
|
1278
|
-
id: input.id,
|
|
1279
|
-
ttlSec: input.ttlSec,
|
|
1280
|
-
callerUserId: ctx.user.id,
|
|
1281
|
-
callerIsAdmin: ctx.user.isAdmin,
|
|
1282
|
-
});
|
|
1283
|
-
if (record.expiresAt === null) {
|
|
1284
|
-
throw (0, user_error_js_1.trpcUserError)('BAD_REQUEST', 'auth.share-never-expiring-extend', undefined, 'A never-expiring share token cannot be extended');
|
|
1285
|
-
}
|
|
1286
|
-
return { id: record.id, expiresAt: record.expiresAt };
|
|
1287
|
-
}
|
|
1288
|
-
catch (error) {
|
|
1289
|
-
const message = error instanceof Error ? error.message : 'Share token extend failed';
|
|
1290
|
-
if (/not found|expired/i.test(message)) {
|
|
1291
|
-
throw new server_1.TRPCError({ code: 'NOT_FOUND', message });
|
|
1292
|
-
}
|
|
1293
|
-
if (/owner or an admin|never-expiring/i.test(message)) {
|
|
1294
|
-
throw new server_1.TRPCError({ code: 'FORBIDDEN', message });
|
|
1295
|
-
}
|
|
1296
|
-
throw new server_1.TRPCError({ code: 'BAD_REQUEST', message });
|
|
1297
|
-
}
|
|
1298
|
-
}),
|
|
1299
|
-
revokeShareToken: trpc_middleware_js_1.protectedProcedure
|
|
1300
|
-
.input(zod_1.z.object({ id: zod_1.z.string().min(1) }))
|
|
1301
|
-
.output(zod_1.z.object({ success: zod_1.z.boolean() }))
|
|
1302
|
-
.mutation(async ({ input, ctx }) => {
|
|
1303
|
-
assertRealUserSession(ctx.user);
|
|
1304
|
-
const unified = await unifiedShare(input.id);
|
|
1305
|
-
if (unified !== null && credentialTokens !== null) {
|
|
1306
|
-
if (!ctx.user.isAdmin && unified.subjectId !== ctx.user.id) {
|
|
1307
|
-
throw new server_1.TRPCError({
|
|
1308
|
-
code: 'FORBIDDEN',
|
|
1309
|
-
message: 'Only the token owner or an admin can revoke a share token',
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
return { success: await credentialTokens.revoke(input.id) };
|
|
1313
|
-
}
|
|
1314
|
-
const service = requireShareTokens();
|
|
1315
|
-
try {
|
|
1316
|
-
const removed = await service.revoke({
|
|
1317
|
-
id: input.id,
|
|
1318
|
-
callerUserId: ctx.user.id,
|
|
1319
|
-
callerIsAdmin: ctx.user.isAdmin,
|
|
1320
|
-
});
|
|
1321
|
-
if (removed) {
|
|
1322
|
-
const internal = registry === null ? null : (0, hub_sessions_js_1.userManagementInternal)(registry);
|
|
1323
|
-
if (internal !== null) {
|
|
1324
|
-
const ip = network.clientAddress(ctx.req).ip;
|
|
1325
|
-
await internal.audit.record({
|
|
1326
|
-
action: 'credentials-revoked',
|
|
1327
|
-
userId: ctx.user.id,
|
|
1328
|
-
...(ip !== null ? { ip } : {}),
|
|
1329
|
-
detail: {
|
|
1330
|
-
kind: 'share-link',
|
|
1331
|
-
requested: 1,
|
|
1332
|
-
revoked: 1,
|
|
1333
|
-
skipped: 0,
|
|
1334
|
-
ids: input.id,
|
|
1335
|
-
},
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
return { success: removed };
|
|
1340
|
-
}
|
|
1341
|
-
catch (error) {
|
|
1342
|
-
throw new server_1.TRPCError({
|
|
1343
|
-
code: 'FORBIDDEN',
|
|
1344
|
-
message: error instanceof Error ? error.message : 'Share token revoke failed',
|
|
1345
|
-
});
|
|
1346
|
-
}
|
|
1347
|
-
}),
|
|
1348
|
-
/**
|
|
1349
|
-
* Share links. `owner: 'self'` — the caller's own (My access);
|
|
1350
|
-
* `owner: 'all'` — every user's, admin only. Omitted keeps the original
|
|
1351
|
-
* answer (own for a user, all for an admin) for clients that predate it.
|
|
1352
|
-
*/
|
|
1353
|
-
listShareTokens: trpc_middleware_js_1.protectedProcedure
|
|
1354
|
-
.input(zod_1.z.object({ owner: zod_1.z.enum(['self', 'all']).optional() }).optional())
|
|
1355
|
-
.output(zod_1.z.array(ShareTokenSummarySchema))
|
|
1356
|
-
.query(async ({ ctx, input }) => {
|
|
1357
|
-
assertRealUserSession(ctx.user);
|
|
1358
|
-
// The legacy `csv_` store, or — on a node with only the unified store —
|
|
1359
|
-
// none; with neither, the clear refusal below.
|
|
1360
|
-
const service = credentialTokens === null ? requireShareTokens() : shareTokens;
|
|
1361
|
-
const owner = input?.owner ?? (ctx.user.isAdmin ? 'all' : 'self');
|
|
1362
|
-
if (owner === 'all' && !ctx.user.isAdmin) {
|
|
1363
|
-
throw new server_1.TRPCError({
|
|
1364
|
-
code: 'FORBIDDEN',
|
|
1365
|
-
message: "Listing every user's share links requires an admin",
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
const records = service === null
|
|
1369
|
-
? []
|
|
1370
|
-
: owner === 'all'
|
|
1371
|
-
? await service.listAll()
|
|
1372
|
-
: await service.listForUser(ctx.user.id);
|
|
1373
|
-
const unified = credentialTokens === null
|
|
1374
|
-
? []
|
|
1375
|
-
: await credentialTokens.list(owner === 'all' ? undefined : ctx.user.id);
|
|
1376
|
-
const unifiedRows = unified.flatMap((record) => {
|
|
1377
|
-
const grant = (0, types_2.shareViewGrantOf)(record.scopes);
|
|
1378
|
-
if (grant === null || record.revokedAt !== null)
|
|
1379
|
-
return [];
|
|
1380
|
-
return [
|
|
1381
|
-
{
|
|
1382
|
-
id: record.id,
|
|
1383
|
-
userId: record.subjectId,
|
|
1384
|
-
tokenPrefix: `token:${record.id.slice(0, 8)}`,
|
|
1385
|
-
scope: grant,
|
|
1386
|
-
createdAt: record.createdAt,
|
|
1387
|
-
expiresAt: record.expiresAt,
|
|
1388
|
-
neverExpires: record.expiresAt === null,
|
|
1389
|
-
...(record.lastUsedAt !== null ? { lastUsedAt: record.lastUsedAt } : {}),
|
|
1390
|
-
legacy: false,
|
|
1391
|
-
},
|
|
1392
|
-
];
|
|
1393
|
-
});
|
|
1394
|
-
return [
|
|
1395
|
-
...unifiedRows,
|
|
1396
|
-
...records.map((record) => ({ ...toShareTokenSummary(record), legacy: true })),
|
|
1397
|
-
];
|
|
1398
|
-
}),
|
|
1399
1133
|
...(0, auth_sessions_procedures_js_1.createSessionProcedures)({ registry, minter, credentialProcedure }),
|
|
1400
1134
|
...(0, auth_device_cert_procedures_js_1.createDeviceCertProcedures)({
|
|
1401
1135
|
registry,
|
|
@@ -1413,9 +1147,9 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
|
|
|
1413
1147
|
issueFor,
|
|
1414
1148
|
}),
|
|
1415
1149
|
...(0, auth_reauth_procedures_js_1.createReauthProcedures)({ auth, registry, moleculer, limiters }),
|
|
1416
|
-
...(0,
|
|
1417
|
-
...(0, auth_tokens_procedures_js_1.createTokenProcedures)({ auth, registry, network, credentialTokens
|
|
1418
|
-
...(0, auth_revoke_credentials_procedures_js_1.createRevokeCredentialsProcedures)({ registry,
|
|
1150
|
+
...(0, auth_audit_procedures_js_1.createAuthAuditProcedures)({ registry }),
|
|
1151
|
+
...(0, auth_tokens_procedures_js_1.createTokenProcedures)({ auth, registry, network, credentialTokens }),
|
|
1152
|
+
...(0, auth_revoke_credentials_procedures_js_1.createRevokeCredentialsProcedures)({ registry, network, credentialTokens }),
|
|
1419
1153
|
// ── External sign-in identities (D782) ────────────────────────
|
|
1420
1154
|
//
|
|
1421
1155
|
// A signed-in user links an OIDC identity to THEIR OWN account. The
|
|
@@ -719,6 +719,28 @@ function buildNodesProvider(agentRegistry, moleculer, addonRegistry, getNodeRoot
|
|
|
719
719
|
function ownerSecrets(ar, owner) {
|
|
720
720
|
return typeof ar.secretsForOwner === 'function' ? ar.secretsForOwner(owner) : null;
|
|
721
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* The integration settings keys the addon's own form stores in the keychain
|
|
724
|
+
* (by schema type, D783) — so the admin read can withhold a plaintext under one
|
|
725
|
+
* of them. Empty when the schema cannot be read (logged): markers are still
|
|
726
|
+
* withheld, and a coerced marker is caught without a schema.
|
|
727
|
+
*/
|
|
728
|
+
async function integrationSecretKeys(ar, addonId, logger) {
|
|
729
|
+
try {
|
|
730
|
+
const provider = ar
|
|
731
|
+
.getCapabilityRegistry()
|
|
732
|
+
.getSingleton('addon-settings');
|
|
733
|
+
const schema = await provider?.getGlobalSettings({ addonId, nodeId: 'hub' });
|
|
734
|
+
return (0, types_1.collectKeychainStoredKeys)(schema ?? null);
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
logger.warn('integration settings: addon schema unreadable — plaintext secrets not detectable', {
|
|
738
|
+
tags: { addonId },
|
|
739
|
+
meta: { phase: 'getSettings', error: (0, types_1.errMsg)(err) },
|
|
740
|
+
});
|
|
741
|
+
return new Set();
|
|
742
|
+
}
|
|
743
|
+
}
|
|
722
744
|
function requireIntegrationRegistry(ar) {
|
|
723
745
|
const reg = ar.getIntegrationRegistry();
|
|
724
746
|
if (!reg)
|
|
@@ -1143,7 +1165,22 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
1143
1165
|
const caller = Reflect.get(input, 'callerAddonId');
|
|
1144
1166
|
const forOwner = typeof caller === 'string' &&
|
|
1145
1167
|
(caller === integration.addonId || ar.runnerIdFor(integration.addonId) === caller);
|
|
1146
|
-
|
|
1168
|
+
// The schema is asked only for the admin read: the owner reads this at
|
|
1169
|
+
// its own boot, and asking it for its schema then would call back into it.
|
|
1170
|
+
const secretKeys = forOwner
|
|
1171
|
+
? undefined
|
|
1172
|
+
: await integrationSecretKeys(ar, integration.addonId, logger);
|
|
1173
|
+
return (0, integration_secrets_1.readIntegrationSettings)({ registry: reg, secrets: forOwner ? ownerSecrets(ar, integration.addonId) : null }, {
|
|
1174
|
+
integrationId: input.id,
|
|
1175
|
+
forOwner,
|
|
1176
|
+
...(secretKeys !== undefined ? { secretKeys } : {}),
|
|
1177
|
+
onSuspectSecret: (suspect) => logger.error(suspect.kind === 'coerced-marker'
|
|
1178
|
+
? 'integration secret is "[object Object]" — a keychain marker was coerced to text; the addon is sending it as the credential (repair: scripts/hub/repair-integration-secret-markers.ts, or re-type it)'
|
|
1179
|
+
: 'integration secret stored as PLAINTEXT under a keychain-declared key — withheld from the form; re-type it to move it into the keychain', {
|
|
1180
|
+
tags: { integrationId: input.id, addonId: integration.addonId },
|
|
1181
|
+
meta: { phase: 'getSettings', key: suspect.key, kind: suspect.kind, forOwner },
|
|
1182
|
+
}),
|
|
1183
|
+
});
|
|
1147
1184
|
},
|
|
1148
1185
|
setSettings: async (input) => {
|
|
1149
1186
|
const reg = requireIntegrationRegistry(ar);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COERCED_MARKER_TEXT = void 0;
|
|
3
4
|
exports.writeIntegrationSettings = writeIntegrationSettings;
|
|
4
5
|
exports.readIntegrationSettings = readIntegrationSettings;
|
|
5
6
|
/**
|
|
@@ -45,16 +46,36 @@ async function writeIntegrationSettings(deps, input) {
|
|
|
45
46
|
if (orphaned.length > 0)
|
|
46
47
|
deps.onOrphaned?.(orphaned);
|
|
47
48
|
}
|
|
49
|
+
/** The text `String(...)` makes of an object — a keychain marker some copy coerced. */
|
|
50
|
+
exports.COERCED_MARKER_TEXT = '[object Object]';
|
|
51
|
+
function suspectKind(key, value, secretKeys) {
|
|
52
|
+
if (value === exports.COERCED_MARKER_TEXT)
|
|
53
|
+
return 'coerced-marker';
|
|
54
|
+
if (secretKeys?.has(key) === true && typeof value === 'string' && value.length > 0) {
|
|
55
|
+
return 'plaintext';
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
48
59
|
async function readIntegrationSettings(deps, input) {
|
|
49
60
|
const stored = await deps.registry.getIntegrationSettings(input.integrationId);
|
|
61
|
+
const suspects = new Set();
|
|
62
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
63
|
+
const kind = suspectKind(key, value, input.secretKeys);
|
|
64
|
+
if (kind === null)
|
|
65
|
+
continue;
|
|
66
|
+
suspects.add(key);
|
|
67
|
+
input.onSuspectSecret?.({ key, kind });
|
|
68
|
+
}
|
|
50
69
|
if (input.forOwner) {
|
|
51
70
|
if (deps.secrets === null)
|
|
52
71
|
return stored;
|
|
53
72
|
return (await (0, types_1.resolveSecretMarkers)(stored, deps.secrets)).value;
|
|
54
73
|
}
|
|
74
|
+
// Anyone but the owner — the admin form — never receives a secret's text:
|
|
75
|
+
// a marker becomes its "set" view, and so does a suspect plaintext.
|
|
55
76
|
const out = {};
|
|
56
77
|
for (const [key, value] of Object.entries(stored)) {
|
|
57
|
-
out[key] = (0, types_1.isSecretMarker)(value) ? (0, types_1.toSecretFieldView)(value) : value;
|
|
78
|
+
out[key] = (0, types_1.isSecretMarker)(value) || suspects.has(key) ? (0, types_1.toSecretFieldView)(value) : value;
|
|
58
79
|
}
|
|
59
80
|
return out;
|
|
60
81
|
}
|