@camstack/server 1.2.580 → 1.2.582

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.
@@ -10,6 +10,7 @@ const zod_1 = require("zod");
10
10
  const session_mint_js_1 = require("../../auth/session-mint.js");
11
11
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
12
12
  const auth_reauth_procedures_js_1 = require("./auth-reauth.procedures.js");
13
+ const user_error_js_1 = require("../trpc/user-error.js");
13
14
  const DeviceLoginResultSchema = zod_1.z.discriminatedUnion('ok', [
14
15
  zod_1.z.object({
15
16
  ok: zod_1.z.literal(true),
@@ -75,10 +76,7 @@ function createDeviceCertProcedures(deps) {
75
76
  ? null
76
77
  : (0, system_1.getUserManagementInternal)(deps.registry.getSingleton('user-management'));
77
78
  if (found === null) {
78
- throw new server_1.TRPCError({
79
- code: 'PRECONDITION_FAILED',
80
- message: 'Device certificates are not available',
81
- });
79
+ throw (0, user_error_js_1.trpcUserError)('PRECONDITION_FAILED', 'device.certificates-unavailable', undefined, 'Device certificates are not available');
82
80
  }
83
81
  return found;
84
82
  };
@@ -113,7 +111,7 @@ function createDeviceCertProcedures(deps) {
113
111
  };
114
112
  const assertPerson = (user) => {
115
113
  if (user.isScoped === true || user.shareView !== undefined || user.adminTokenId !== undefined) {
116
- throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'Requires a signed-in user session' });
114
+ throw (0, user_error_js_1.trpcUserError)('FORBIDDEN', 'auth.session-required', undefined, 'Requires a signed-in user session');
117
115
  }
118
116
  };
119
117
  const selfLimit = (userId, path) => {
@@ -8,6 +8,7 @@ const zod_1 = require("zod");
8
8
  const passkey_provider_js_1 = require("../../auth/passkey-provider.js");
9
9
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
10
10
  const auth_limits_js_1 = require("./auth-limits.js");
11
+ const user_error_js_1 = require("../trpc/user-error.js");
11
12
  /** The input field a re-auth-gated procedure takes. */
12
13
  exports.ReauthTokenInput = zod_1.z.string().min(1).max(4096);
13
14
  exports.REAUTH_REQUIRED_MESSAGE = 'Confirm it is you first: re-enter your password or use a passkey (update the app if it cannot ask).';
@@ -31,7 +32,7 @@ function isPasswordChecker(value) {
31
32
  /** Real user sessions only: a scoped or share token cannot prove a password. */
32
33
  function assertPerson(user) {
33
34
  if (user.isScoped === true || user.shareView !== undefined || user.adminTokenId !== undefined) {
34
- throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'Requires a signed-in user session' });
35
+ throw (0, user_error_js_1.trpcUserError)('FORBIDDEN', 'auth.session-required', undefined, 'Requires a signed-in user session');
35
36
  }
36
37
  }
37
38
  function createReauthProcedures(deps) {
@@ -79,7 +80,7 @@ function createReauthProcedures(deps) {
79
80
  });
80
81
  if (record === null || record.id !== ctx.user.id) {
81
82
  (0, auth_limits_js_1.recordAccountFailure)(deps.limiters, ctx.user.username);
82
- throw new server_1.TRPCError({ code: 'UNAUTHORIZED', message: 'Password is incorrect' });
83
+ throw (0, user_error_js_1.trpcUserError)('UNAUTHORIZED', 'auth.password-incorrect', undefined, 'Password is incorrect');
83
84
  }
84
85
  method = 'password';
85
86
  }
@@ -93,7 +94,7 @@ function createReauthProcedures(deps) {
93
94
  });
94
95
  if (!result.verified) {
95
96
  (0, auth_limits_js_1.recordAccountFailure)(deps.limiters, ctx.user.username);
96
- throw new server_1.TRPCError({ code: 'UNAUTHORIZED', message: 'Passkey verification failed' });
97
+ throw (0, user_error_js_1.trpcUserError)('UNAUTHORIZED', 'auth.passkey-verification-failed', undefined, 'Passkey verification failed');
97
98
  }
98
99
  method = 'passkey';
99
100
  }
@@ -8,6 +8,7 @@ const types_1 = require("@camstack/types");
8
8
  const hub_sessions_js_1 = require("../../auth/hub-sessions.js");
9
9
  const session_mint_js_1 = require("../../auth/session-mint.js");
10
10
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
11
+ const user_error_js_1 = require("../trpc/user-error.js");
11
12
  /** One login session as the UI lists it. */
12
13
  exports.LoginSessionSchema = zod_1.z.object({
13
14
  id: zod_1.z.string(),
@@ -51,7 +52,7 @@ function requireSessions(registry) {
51
52
  /** Real user sessions only — a scoped or share token has no login session to manage. */
52
53
  function assertLoginSession(user) {
53
54
  if (user.isScoped === true || user.shareView !== undefined || user.adminTokenId !== undefined) {
54
- throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'Requires a signed-in user session' });
55
+ throw (0, user_error_js_1.trpcUserError)('FORBIDDEN', 'auth.session-required', undefined, 'Requires a signed-in user session');
55
56
  }
56
57
  }
57
58
  function createSessionProcedures(deps) {
@@ -42,6 +42,8 @@ const auth_reauth_procedures_js_1 = require("./auth-reauth.procedures.js");
42
42
  const auth_device_cert_procedures_js_1 = require("./auth-device-cert.procedures.js");
43
43
  const auth_sessions_procedures_js_1 = require("./auth-sessions.procedures.js");
44
44
  const server_name_js_1 = require("./server-name.js");
45
+ const types_2 = require("@camstack/types");
46
+ const user_error_js_1 = require("../trpc/user-error.js");
45
47
  /**
46
48
  * A public procedure limited per (procedure, REAL client address) — see
47
49
  * `auth-limits.ts` for the tiers and why the address is the forwarded client.
@@ -351,7 +353,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
351
353
  }
352
354
  catch (error) {
353
355
  if (error instanceof session_mint_js_1.SessionUserGoneError) {
354
- throw new server_1.TRPCError({ code: 'UNAUTHORIZED', message: 'User no longer exists' });
356
+ throw (0, user_error_js_1.trpcUserError)('UNAUTHORIZED', 'auth.user-gone', undefined, 'User no longer exists');
355
357
  }
356
358
  throw error;
357
359
  }
@@ -582,7 +584,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
582
584
  });
583
585
  if (!user) {
584
586
  (0, auth_limits_js_1.recordAccountFailure)(limiters, input.username);
585
- throw new Error('Invalid credentials');
587
+ throw (0, types_2.userError)('auth.invalid-credentials', undefined, 'Invalid credentials');
586
588
  }
587
589
  // ── Second-factor gate ───────────────────────────────────────
588
590
  // After credentials validate, collect every ACTIVE second
@@ -644,7 +646,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
644
646
  : { valid: false };
645
647
  if (!verify.valid) {
646
648
  (0, auth_limits_js_1.recordAccountFailure)(limiters, claims.username);
647
- throw new Error('Invalid TOTP code');
649
+ throw (0, types_2.userError)('auth.invalid-totp', undefined, 'Invalid TOTP code');
648
650
  }
649
651
  return mintSessionForUserId(claims.userId, issueFor(ctx.req, claims.method ?? 'password', input.refreshable));
650
652
  }),
@@ -661,7 +663,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
661
663
  .mutation(async ({ input }) => {
662
664
  const claims = auth.verifyTotpChallengeToken(input.challengeToken);
663
665
  if (!claims) {
664
- throw new Error('Invalid or expired login challenge — please re-enter your password');
666
+ throw (0, types_2.userError)('auth.login-challenge-expired', undefined, 'Invalid or expired login challenge — please re-enter your password');
665
667
  }
666
668
  const provider = (0, passkey_provider_js_1.resolvePasskeyProvider)(registry, moleculer);
667
669
  if (!provider)
@@ -685,7 +687,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
685
687
  .mutation(async ({ input, ctx }) => {
686
688
  const claims = auth.verifyTotpChallengeToken(input.challengeToken);
687
689
  if (!claims) {
688
- throw new Error('Invalid or expired login challenge — please re-enter your password');
690
+ throw (0, types_2.userError)('auth.login-challenge-expired', undefined, 'Invalid or expired login challenge — please re-enter your password');
689
691
  }
690
692
  const provider = (0, passkey_provider_js_1.resolvePasskeyProvider)(registry, moleculer);
691
693
  if (!provider)
@@ -697,7 +699,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
697
699
  });
698
700
  if (!result.verified) {
699
701
  (0, auth_limits_js_1.recordAccountFailure)(limiters, claims.username);
700
- throw new Error('Passkey verification failed');
702
+ throw (0, types_2.userError)('auth.passkey-verification-failed', undefined, 'Passkey verification failed');
701
703
  }
702
704
  return mintSessionForUserId(claims.userId, issueFor(ctx.req, claims.method ?? 'password', input.refreshable));
703
705
  }),
@@ -747,7 +749,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
747
749
  response: input.response,
748
750
  });
749
751
  if (!result.verified || !result.userId) {
750
- throw new server_1.TRPCError({ code: 'UNAUTHORIZED', message: 'Passkey verification failed' });
752
+ throw (0, user_error_js_1.trpcUserError)('UNAUTHORIZED', 'auth.passkey-verification-failed', undefined, 'Passkey verification failed');
751
753
  }
752
754
  const secondFactors = await factorsOwedHere(result.userId, 'passkey', ctx.req);
753
755
  if (secondFactors.length > 0) {
@@ -803,14 +805,14 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
803
805
  }
804
806
  const cookieToken = (0, session_cookie_js_1.readSessionCookieFromHeader)(readRequestHeader(ctx.req, 'cookie') ?? undefined);
805
807
  if (!cookieToken) {
806
- throw new server_1.TRPCError({ code: 'UNAUTHORIZED', message: 'No session cookie' });
808
+ throw (0, user_error_js_1.trpcUserError)('UNAUTHORIZED', 'auth.no-session-cookie', undefined, 'No session cookie');
807
809
  }
808
810
  let payload;
809
811
  try {
810
812
  payload = auth.verifyToken(cookieToken);
811
813
  }
812
814
  catch {
813
- throw new server_1.TRPCError({ code: 'UNAUTHORIZED', message: 'Invalid session cookie' });
815
+ throw (0, user_error_js_1.trpcUserError)('UNAUTHORIZED', 'auth.invalid-session-cookie', undefined, 'Invalid session cookie');
814
816
  }
815
817
  // Reject non-session JWTs that verify under the same secret:
816
818
  // challenge/bridge tokens carry a `kind` discriminator, and a
@@ -819,7 +821,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
819
821
  if (kind !== undefined ||
820
822
  typeof payload.isAdmin !== 'boolean' ||
821
823
  typeof payload.userId !== 'string') {
822
- throw new server_1.TRPCError({ code: 'UNAUTHORIZED', message: 'Invalid session cookie' });
824
+ throw (0, user_error_js_1.trpcUserError)('UNAUTHORIZED', 'auth.invalid-session-cookie', undefined, 'Invalid session cookie');
823
825
  }
824
826
  // A session of its own for the client that asked (the hub-served
825
827
  // viewer): signing out of one never signs the other out.
@@ -912,10 +914,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
912
914
  });
913
915
  }
914
916
  if (!grant || grant.purpose === 'step-up') {
915
- throw new server_1.TRPCError({
916
- code: 'UNAUTHORIZED',
917
- message: 'Invalid or expired handoff code',
918
- });
917
+ throw (0, user_error_js_1.trpcUserError)('UNAUTHORIZED', 'auth.handoff-code-invalid', undefined, 'Invalid or expired handoff code');
919
918
  }
920
919
  // A `renew` code continues THE SESSION IT WAS MINTED FROM. One without a session would
921
920
  // open a brand-new session with no PKCE behind it: refused.
@@ -923,10 +922,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
923
922
  options.logger?.warn('Handoff redeem refused: a renew code with no session behind it', {
924
923
  tags: { userId: grant.userId },
925
924
  });
926
- throw new server_1.TRPCError({
927
- code: 'UNAUTHORIZED',
928
- message: 'Invalid or expired handoff code',
929
- });
925
+ throw (0, user_error_js_1.trpcUserError)('UNAUTHORIZED', 'auth.handoff-code-invalid', undefined, 'Invalid or expired handoff code');
930
926
  }
931
927
  if (grant.purpose === 'renew' && grant.sessionId !== undefined) {
932
928
  // A legacy client renewing: the SAME session, its expiry slides. A
@@ -1015,7 +1011,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1015
1011
  if (!userMgmt)
1016
1012
  throw new Error('user-management capability not available');
1017
1013
  if (!ctx.user)
1018
- throw new Error('Not authenticated');
1014
+ throw (0, types_2.userError)('auth.not-authenticated', undefined, 'Not authenticated');
1019
1015
  // Re-validate the current password against the live store to
1020
1016
  // confirm session identity → password ownership match. Stops a
1021
1017
  // stolen session-token from rotating credentials silently.
@@ -1045,7 +1041,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1045
1041
  if (!userMgmt)
1046
1042
  throw new Error('user-management capability not available');
1047
1043
  if (!ctx.user)
1048
- throw new Error('Not authenticated');
1044
+ throw (0, types_2.userError)('auth.not-authenticated', undefined, 'Not authenticated');
1049
1045
  return userMgmt.setupTotp({ userId: ctx.user.id });
1050
1046
  }),
1051
1047
  confirmOwnTotp: trpc_middleware_js_1.protectedProcedure
@@ -1056,7 +1052,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1056
1052
  if (!userMgmt)
1057
1053
  throw new Error('user-management capability not available');
1058
1054
  if (!ctx.user)
1059
- throw new Error('Not authenticated');
1055
+ throw (0, types_2.userError)('auth.not-authenticated', undefined, 'Not authenticated');
1060
1056
  return userMgmt.confirmTotp({ userId: ctx.user.id, code: input.code });
1061
1057
  }),
1062
1058
  disableOwnTotp: trpc_middleware_js_1.protectedProcedure
@@ -1069,7 +1065,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1069
1065
  if (!userMgmt)
1070
1066
  throw new Error('user-management capability not available');
1071
1067
  if (!ctx.user)
1072
- throw new Error('Not authenticated');
1068
+ throw (0, types_2.userError)('auth.not-authenticated', undefined, 'Not authenticated');
1073
1069
  return userMgmt.disableTotp({ userId: ctx.user.id });
1074
1070
  }),
1075
1071
  getOwnTotpStatus: trpc_middleware_js_1.protectedProcedure
@@ -1094,7 +1090,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1094
1090
  .output(passkey_provider_js_1.OptionsJsonSchema)
1095
1091
  .mutation(async ({ ctx }) => {
1096
1092
  if (!ctx.user)
1097
- throw new Error('Not authenticated');
1093
+ throw (0, types_2.userError)('auth.not-authenticated', undefined, 'Not authenticated');
1098
1094
  const provider = (0, passkey_provider_js_1.resolvePasskeyProvider)(registry, moleculer);
1099
1095
  if (!provider)
1100
1096
  throw new Error('Passkey enrollment is not available');
@@ -1105,7 +1101,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1105
1101
  .output(passkey_provider_js_1.FinishRegistrationSchema)
1106
1102
  .mutation(async ({ input, ctx }) => {
1107
1103
  if (!ctx.user)
1108
- throw new Error('Not authenticated');
1104
+ throw (0, types_2.userError)('auth.not-authenticated', undefined, 'Not authenticated');
1109
1105
  const provider = (0, passkey_provider_js_1.resolvePasskeyProvider)(registry, moleculer);
1110
1106
  if (!provider)
1111
1107
  throw new Error('Passkey enrollment is not available');
@@ -1137,7 +1133,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1137
1133
  .output(zod_1.z.object({ success: zod_1.z.literal(true) }))
1138
1134
  .mutation(async ({ input, ctx }) => {
1139
1135
  if (!ctx.user)
1140
- throw new Error('Not authenticated');
1136
+ throw (0, types_2.userError)('auth.not-authenticated', undefined, 'Not authenticated');
1141
1137
  (0, auth_reauth_procedures_js_1.assertReauthenticated)(auth, ctx.user, input.reauthToken);
1142
1138
  const provider = (0, passkey_provider_js_1.resolvePasskeyProvider)(registry, moleculer);
1143
1139
  if (!provider)
@@ -1173,7 +1169,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1173
1169
  .output(passkey_provider_js_1.SetSecondFactorPreferenceSchema)
1174
1170
  .mutation(async ({ input, ctx }) => {
1175
1171
  if (!ctx.user)
1176
- throw new Error('Not authenticated');
1172
+ throw (0, types_2.userError)('auth.not-authenticated', undefined, 'Not authenticated');
1177
1173
  const provider = (0, passkey_provider_js_1.resolvePasskeyProvider)(registry, moleculer);
1178
1174
  if (!provider)
1179
1175
  throw new Error('Passkey management is not available');
@@ -1243,10 +1239,7 @@ function createAuthRouter(auth, registry, moleculer = null, shareTokens = null,
1243
1239
  callerIsAdmin: ctx.user.isAdmin,
1244
1240
  });
1245
1241
  if (record.expiresAt === null) {
1246
- throw new server_1.TRPCError({
1247
- code: 'BAD_REQUEST',
1248
- message: 'A never-expiring share token cannot be extended',
1249
- });
1242
+ throw (0, user_error_js_1.trpcUserError)('BAD_REQUEST', 'auth.share-never-expiring-extend', undefined, 'A never-expiring share token cannot be extended');
1250
1243
  }
1251
1244
  return { id: record.id, expiresAt: record.expiresAt };
1252
1245
  }
@@ -12,6 +12,7 @@ const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
12
12
  const share_view_access_js_1 = require("../trpc/share-view-access.js");
13
13
  const principal_visibility_js_1 = require("../trpc/principal-visibility.js");
14
14
  const public_event_filter_js_1 = require("./public-event-filter.js");
15
+ const types_1 = require("@camstack/types");
15
16
  // The pushed wire shape is `SystemEvent` from @camstack/types — the exact
16
17
  // type `EventBusService.subscribe` hands its handlers. It MUST be a type
17
18
  // exported from @camstack/types (not a local interface): the AppRouter
@@ -82,22 +83,22 @@ function createLiveEventsRouter(eb, ar) {
82
83
  const deviceRegistry = ar.getDeviceRegistry();
83
84
  const device = deviceRegistry.getById(input.deviceId);
84
85
  if (!device)
85
- throw new Error(`Device id=${input.deviceId} not found`);
86
+ throw (0, types_1.userError)('device.not-found', { id: input.deviceId }, `Device id=${input.deviceId} not found`);
86
87
  // Permission check — new IDevice has no providerId; admin bypass only
87
88
  if (!ctx.user.isAdmin) {
88
89
  const ad = ctx.user.permissions?.allowedDevices;
89
90
  if (!ad)
90
- throw new Error('Access denied');
91
+ throw (0, types_1.userError)('auth.access-denied', undefined, 'Access denied');
91
92
  // Find which addon owns this device to determine the provider key
92
93
  const ownerAddonId = deviceRegistry.getAddonId(input.deviceId);
93
94
  if (!ownerAddonId)
94
- throw new Error('Access denied');
95
+ throw (0, types_1.userError)('auth.access-denied', undefined, 'Access denied');
95
96
  const pd = ad[ownerAddonId];
96
97
  // Permission lists are still keyed by stableId — that's the
97
98
  // external admin-facing identifier. Resolve the device's
98
99
  // stableId for the membership check.
99
100
  if (!pd || (pd !== '*' && !pd.includes(device.stableId))) {
100
- throw new Error('Access denied');
101
+ throw (0, types_1.userError)('auth.access-denied', undefined, 'Access denied');
101
102
  }
102
103
  }
103
104
  return eb.subscribe({ source: { type: 'device', id: input.deviceId } }, (evt) => {
@@ -17,6 +17,7 @@ exports.formatTrpcError = formatTrpcError;
17
17
  * The formatter is EXPORTED for unit testing (no side-effects, pure function).
18
18
  */
19
19
  const system_1 = require("@camstack/system");
20
+ const types_1 = require("@camstack/types");
20
21
  // ---------------------------------------------------------------------------
21
22
  // Type guards
22
23
  // ---------------------------------------------------------------------------
@@ -110,6 +111,29 @@ function extractCapRouteError(err) {
110
111
  * Returns the shape unchanged for all other errors.
111
112
  */
112
113
  function formatTrpcError(opts) {
114
+ return withErrorI18n(opts, withCapRouteData(opts));
115
+ }
116
+ /**
117
+ * A coded error (`userError(...)`, D863) gets its response `message` in the
118
+ * request language when the `server` catalog has the code; the code and params
119
+ * stay in `data.i18n` whatever the language. A legacy error (no code), an
120
+ * unknown code and English all pass through with the original message.
121
+ */
122
+ function withErrorI18n(opts, shape) {
123
+ const coded = (0, types_1.readErrorI18n)(opts.error);
124
+ if (coded === null)
125
+ return shape;
126
+ const lang = opts.ctx?.lang ?? types_1.DEFAULT_CATALOG_LANG;
127
+ const text = lang === types_1.DEFAULT_CATALOG_LANG
128
+ ? undefined
129
+ : (0, system_1.getCatalogRegistry)().lookup(lang, system_1.SERVER_NAMESPACE, coded.code);
130
+ return {
131
+ ...shape,
132
+ ...(text !== undefined ? { message: (0, types_1.interpolate)(text, coded.params) } : {}),
133
+ data: { ...shape.data, i18n: coded },
134
+ };
135
+ }
136
+ function withCapRouteData(opts) {
113
137
  const { error, shape } = opts;
114
138
  // extractCapRouteError already walks the full .cause chain, so a single call
115
139
  // starting from `error` covers both `error instanceof CapRouteError` and
@@ -2,7 +2,7 @@
2
2
  // AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
3
3
  // Re-run: npx tsx scripts/generate-cap-mounts.ts
4
4
  //
5
- // Mounted: 170 Skipped (legacy): 3
5
+ // Mounted: 171 Skipped (legacy): 3
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.LEGACY_SHAPE_SKIP = void 0;
8
8
  exports.mountAllCaps = mountAllCaps;
@@ -228,6 +228,22 @@ function mountAllCaps(services) {
228
228
  discoverDevices: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'discoverDevices'),
229
229
  };
230
230
  }, remoteCapProxy),
231
+ deviceRemovalHook: (0, generated_cap_routers_js_1.createCapRouter_deviceRemovalHook)((_ctx, addonId) => {
232
+ if (!reg)
233
+ return null;
234
+ if (addonId !== undefined) {
235
+ return reg.getProviderByAddonId('device-removal-hook', addonId);
236
+ }
237
+ const entries = reg.getCollectionEntries('device-removal-hook');
238
+ if (entries.length === 0)
239
+ return null;
240
+ const providers = entries.map(([, p]) => p);
241
+ const first = providers[0];
242
+ return {
243
+ ...first,
244
+ listDeviceData: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listDeviceData'),
245
+ };
246
+ }, remoteCapProxy),
231
247
  deviceState: (0, generated_cap_routers_js_1.createCapRouter_deviceState)((_ctx) => reg?.getSingleton('device-state') ?? null, remoteCapProxy),
232
248
  deviceStatus: (0, generated_cap_routers_js_1.createCapRouter_deviceStatus)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'device-status'), remoteCapProxy),
233
249
  doorbell: (0, generated_cap_routers_js_1.createCapRouter_doorbell)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'doorbell'), remoteCapProxy),