@dereekb/firebase 13.30.0 → 13.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/firebase",
3
- "version": "13.30.0",
3
+ "version": "13.32.0",
4
4
  "sideEffects": false,
5
5
  "exports": {
6
6
  "./test": {
@@ -24,10 +24,10 @@
24
24
  }
25
25
  },
26
26
  "peerDependencies": {
27
- "@dereekb/date": "13.30.0",
28
- "@dereekb/model": "13.30.0",
29
- "@dereekb/rxjs": "13.30.0",
30
- "@dereekb/util": "13.30.0",
27
+ "@dereekb/date": "13.32.0",
28
+ "@dereekb/model": "13.32.0",
29
+ "@dereekb/rxjs": "13.32.0",
30
+ "@dereekb/util": "13.32.0",
31
31
  "@firebase/rules-unit-testing": "5.0.0",
32
32
  "@marcbachmann/cel-js": "^7.6.1",
33
33
  "@typescript-eslint/parser": "8.59.3",
@@ -63,11 +63,49 @@ export interface OidcProviderProfile<S extends OidcScope = OidcScope> {
63
63
  * Optional human-readable description, used in the admin profile picker.
64
64
  */
65
65
  readonly description?: string;
66
+ /**
67
+ * Whether this profile applies to a client that has NO profiles assigned (an empty or absent
68
+ * `dbx_provider_profiles`). Defaults to `false`.
69
+ *
70
+ * Lets an app make a coarse, broadly-available scope profile-gated — so the profile picker becomes
71
+ * the single control surface for scope grouping — without breaking every already-registered client.
72
+ * Multiple default profiles union.
73
+ *
74
+ * The fallback is exclusive: a client assigned ANY profile resolves to exactly its assigned
75
+ * profiles, so assigning a non-default profile does NOT additionally confer the default's scopes.
76
+ *
77
+ * Note a `require: 'required'` scope on a default profile is force-required for EVERY unassigned
78
+ * client — a default profile usually wants `require: 'none'`.
79
+ *
80
+ * @see oidcProviderProfilesForClient
81
+ */
82
+ readonly isDefault?: boolean;
83
+ /**
84
+ * Whether the scopes this profile unlocks may only be granted to admin users. Defaults to `false`.
85
+ *
86
+ * Equivalent to listing this profile's scopes in `OidcProviderConfig.adminOnlyScopes` — the two are
87
+ * unioned — but declared alongside the profile so the fact lives in one place rather than drifting
88
+ * between the shared registry and the server provider config.
89
+ *
90
+ * Combined with `require: 'required'`, this makes the whole client admin-only: the client always
91
+ * requests the scope, so a non-admin resolving its consent is always rejected with `access_denied`.
92
+ *
93
+ * Independent of {@link isDefault} and of the profile unlock gate — a scope may be subject to both
94
+ * gates.
95
+ *
96
+ * @see adminOnlyScopesForOidcProviderProfiles
97
+ */
98
+ readonly adminOnly?: boolean;
66
99
  /**
67
100
  * The scopes this profile unlocks, each with an optional require mode.
68
101
  */
69
102
  readonly scopes: readonly OidcProviderProfileScopeConfig<S>[];
70
103
  }
104
+ /**
105
+ * Suffix appended to a default profile's description in {@link oidcProviderProfileDetails}, so an
106
+ * admin viewing the picker sees that leaving the field empty still grants that profile's scopes.
107
+ */
108
+ export declare const OIDC_PROVIDER_PROFILE_DEFAULT_DESCRIPTION_SUFFIX = "Applied by default when no profiles are assigned.";
71
109
  /**
72
110
  * Profile picker entry (label + key + description), mirroring {@link OidcScopeDetails}.
73
111
  */
@@ -80,6 +118,31 @@ export type OidcProviderProfileDetails = LabeledValueWithDescription<OidcProvide
80
118
  * @returns The registry profiles whose key is in `keys`.
81
119
  */
82
120
  export declare function oidcProviderProfilesForKeys<S extends OidcScope = OidcScope>(profiles: readonly OidcProviderProfile<S>[], keys: readonly OidcProviderProfileKey[] | undefined): OidcProviderProfile<S>[];
121
+ /**
122
+ * Filters the provider-profile registry to the profiles marked {@link OidcProviderProfile.isDefault}.
123
+ *
124
+ * @param profiles - The full provider-profile registry.
125
+ * @returns The registry profiles that apply to a client with no assigned profiles.
126
+ */
127
+ export declare function defaultOidcProviderProfiles<S extends OidcScope = OidcScope>(profiles: readonly OidcProviderProfile<S>[]): OidcProviderProfile<S>[];
128
+ /**
129
+ * Resolves the profiles that apply to a client: its assigned profiles, or — when it has NO profiles
130
+ * assigned — the registry's default profiles.
131
+ *
132
+ * The fallback is exclusive: a client with any assigned key resolves to exactly
133
+ * {@link oidcProviderProfilesForKeys}, so a non-default assignment never additionally confers the
134
+ * default profiles' scopes. A registry declaring no default behaves identically to
135
+ * {@link oidcProviderProfilesForKeys}.
136
+ *
137
+ * The fallback keys off the assigned key list being empty/absent rather than off the resolved set
138
+ * being empty, so a client whose assigned profile was later removed from the registry resolves to no
139
+ * profiles (fail-closed) rather than silently picking up the default.
140
+ *
141
+ * @param profiles - The full provider-profile registry.
142
+ * @param keys - The profile keys assigned to the client (its `dbx_provider_profiles`).
143
+ * @returns The client's assigned profiles, or the default profiles when none are assigned.
144
+ */
145
+ export declare function oidcProviderProfilesForClient<S extends OidcScope = OidcScope>(profiles: readonly OidcProviderProfile<S>[], keys: readonly OidcProviderProfileKey[] | undefined): OidcProviderProfile<S>[];
83
146
  /**
84
147
  * Collects every scope referenced by the given profiles.
85
148
  *
@@ -87,6 +150,11 @@ export declare function oidcProviderProfilesForKeys<S extends OidcScope = OidcSc
87
150
  * obtain via a profile. Passed a client's assigned profiles, this is the set of scopes those
88
151
  * profiles unlock for that client.
89
152
  *
153
+ * Note a gated scope is not necessarily unavailable to an unassigned client: a scope unlocked by a
154
+ * default profile is gated yet reachable by every client. Use
155
+ * {@link assignmentOnlyScopesForOidcProviderProfiles} for the "requires an explicit assignment"
156
+ * subset (e.g. to exclude scopes from a general picker or from advertised scope metadata).
157
+ *
90
158
  * @param profiles - The profiles to collect scopes from.
91
159
  * @returns The union of every profile's scopes.
92
160
  */
@@ -98,9 +166,47 @@ export declare function scopesForOidcProviderProfiles<S extends OidcScope = Oidc
98
166
  * @returns The union of every profile's `required` scopes.
99
167
  */
100
168
  export declare function requiredScopesForOidcProviderProfiles<S extends OidcScope = OidcScope>(profiles: readonly OidcProviderProfile<S>[]): Set<S>;
169
+ /**
170
+ * Collects the scopes unlocked by the registry's default profiles — the scopes every client can
171
+ * obtain, including one with no profiles assigned.
172
+ *
173
+ * @param profiles - The full provider-profile registry.
174
+ * @returns The union of every default profile's scopes. Empty when no profile is marked default.
175
+ */
176
+ export declare function defaultUnlockedScopesForOidcProviderProfiles<S extends OidcScope = OidcScope>(profiles: readonly OidcProviderProfile<S>[]): Set<S>;
177
+ /**
178
+ * Collects the gated scopes that are NOT unlocked by default — the scopes a client can only obtain
179
+ * via an explicit profile assignment.
180
+ *
181
+ * This is the set to exclude from a general scope picker or from advertised scope metadata (e.g. an
182
+ * MCP protected-resource document's `scopes_supported`). Prefer it over
183
+ * {@link scopesForOidcProviderProfiles} for that job: the full gated set would wrongly drop a
184
+ * default-unlocked scope that every client can in fact obtain. With no default declared the two are
185
+ * identical.
186
+ *
187
+ * @param profiles - The full provider-profile registry.
188
+ * @returns Every profile-gated scope minus the default-unlocked ones.
189
+ */
190
+ export declare function assignmentOnlyScopesForOidcProviderProfiles<S extends OidcScope = OidcScope>(profiles: readonly OidcProviderProfile<S>[]): Set<S>;
191
+ /**
192
+ * Collects the scopes of every profile marked {@link OidcProviderProfile.adminOnly}.
193
+ *
194
+ * Unioned with `OidcProviderConfig.adminOnlyScopes` by the consent admin-only gate: a consent
195
+ * requesting one of these scopes is hard-rejected with `access_denied` when the resolving user is
196
+ * not an admin.
197
+ *
198
+ * @param profiles - The full provider-profile registry.
199
+ * @returns The union of every admin-only profile's scopes. Empty when no profile is marked admin-only.
200
+ */
201
+ export declare function adminOnlyScopesForOidcProviderProfiles<S extends OidcScope = OidcScope>(profiles: readonly OidcProviderProfile<S>[]): Set<S>;
101
202
  /**
102
203
  * Builds picker entries for the given provider profiles, suitable for an admin profile-selection field.
103
204
  *
205
+ * A default profile's description carries {@link OIDC_PROVIDER_PROFILE_DEFAULT_DESCRIPTION_SUFFIX} so
206
+ * an admin isn't surprised that an empty selection still grants scopes. Default profiles are
207
+ * deliberately not pre-selected — persisting the default as an explicit assignment would opt the
208
+ * client out of the fallback, so it would stop tracking the registry if the default later changed.
209
+ *
104
210
  * @param profiles - The provider-profile registry.
105
211
  * @returns One {@link OidcProviderProfileDetails} per profile.
106
212
  */
@@ -9,6 +9,8 @@ export * from './notification.create.loggedevent';
9
9
  export * from './notification.loggedevent.loader';
10
10
  export * from './notification.create.task';
11
11
  export * from './notification.details';
12
+ export * from './notification.healthcheck';
13
+ export * from './notification.healthcheck.mailgun';
12
14
  export * from './notification.item';
13
15
  export * from './notification.id';
14
16
  export * from './notification.message';
@@ -13,6 +13,7 @@ import { type E164PhoneNumber, type EmailAddress, type IndexNumber, type Maybe }
13
13
  import { type NotificationTypes } from './notification';
14
14
  import { type NotificationUserDefaultNotificationBoxRecipientConfig, type NotificationBoxRecipientTemplateConfigArrayEntry, NotificationBoxRecipientFlag } from './notification.config';
15
15
  import { type NotificationBoxId, type NotificationSummaryId, type NotificationTemplateType } from './notification.id';
16
+ import { type NotificationHealthCheck, NotificationDeliveryMethod } from './notification.healthcheck';
16
17
  import { type NotificationSendEmailMessagesResult, type NotificationSendTextMessagesResult, type NotificationSendNotificationSummaryMessagesResult } from './notification.send';
17
18
  import { type NotificationTaskServiceTaskHandlerCompletionType } from './notification.task';
18
19
  export declare const NOTIFICATION_RECIPIENT_NAME_MIN_LENGTH = 0;
@@ -95,6 +96,101 @@ export { targetModelParamsType as resyncNotificationUserParamsType } from '../..
95
96
  export interface ResyncNotificationUserResult {
96
97
  readonly notificationBoxesUpdated: number;
97
98
  }
99
+ /**
100
+ * Used for running a notification delivery health check for a user.
101
+ *
102
+ * The check always inspects the user's configuration, their notification box subscriptions, and whatever
103
+ * read-only diagnostics each configured delivery method's send service offers. Actually dispatching a
104
+ * test message is opt-in via `sendProbe`, since that delivers real mail/SMS to the user.
105
+ */
106
+ export interface NotificationUserHealthCheckParams extends TargetModelParams {
107
+ /**
108
+ * Restrict the check to these delivery methods. Defaults to every method the server has a send
109
+ * service configured for.
110
+ */
111
+ readonly methods?: Maybe<NotificationDeliveryMethod[]>;
112
+ /**
113
+ * Dispatch a real test message through each checked method that supports probing.
114
+ *
115
+ * Defaults to false. Delivery confirmation is asynchronous, so a dispatched probe is usually still
116
+ * pending when the check returns and is resolved by a later {@link verifyPendingProbesOnly} run.
117
+ */
118
+ readonly sendProbe?: Maybe<boolean>;
119
+ /**
120
+ * Only resolve probes left pending by the previous check, skipping the configuration, history, and
121
+ * provider diagnostics.
122
+ *
123
+ * Defaults to false. This is the POLL that settles an in-flight test message, and it is deliberately
124
+ * cheap enough to be called repeatedly: it consults a provider only for a method that actually has a
125
+ * probe in flight, and touches nothing else. It answers to its own short window rather than the run
126
+ * window, and advances the stored check's `vat` rather than its `at`, so polling a test message never
127
+ * consumes the user's allowance for running the check itself.
128
+ */
129
+ readonly verifyPendingProbesOnly?: Maybe<boolean>;
130
+ /**
131
+ * The notification template type to evaluate per-template configuration against.
132
+ *
133
+ * Defaults to the app's default template type.
134
+ */
135
+ readonly notificationTemplateType?: Maybe<NotificationTemplateType>;
136
+ /**
137
+ * Skip inspecting the user's notification box subscriptions.
138
+ *
139
+ * Defaults to false. Skipping avoids a document read per subscription, at the cost of not detecting
140
+ * a subscription that is broken, still being set up, or out of sync with the user's settings.
141
+ */
142
+ readonly skipSubscriptionChecks?: Maybe<boolean>;
143
+ /**
144
+ * Ignore the run and test message throttle windows.
145
+ *
146
+ * PRIVILEGED — admin only. The windows exist to stop a user from repeatedly hammering the delivery
147
+ * providers and their own inbox, so this must never be honoured for an ordinary caller. The action
148
+ * itself cannot tell who is calling, so the API layer that can is responsible for clearing this
149
+ * before the params reach it:
150
+ *
151
+ * ```ts
152
+ * const params = isAdminInRequest(request) ? data : { ...data, force: undefined };
153
+ * ```
154
+ *
155
+ * Defaults to false.
156
+ */
157
+ readonly force?: Maybe<boolean>;
158
+ /**
159
+ * Return the complete health check rather than just the part the call was about.
160
+ *
161
+ * A `sendProbe` call is about one delivery method's test message, so by default its result carries only
162
+ * the methods it actually probed — the caller already has the rest, and a client rendering the report
163
+ * reads it from the {@link NotificationUser} document instead. Set this to get the whole check back.
164
+ *
165
+ * Defaults to false. Has no effect on a plain run, which always returns everything, and never affects
166
+ * what is persisted: the stored check is always the complete picture.
167
+ */
168
+ readonly returnFullHealthCheck?: Maybe<boolean>;
169
+ }
170
+ export declare const notificationUserHealthCheckParamsType: Type<NotificationUserHealthCheckParams>;
171
+ /**
172
+ * The result of a `healthCheck` invocation.
173
+ */
174
+ export interface NotificationUserHealthCheckResult {
175
+ /**
176
+ * The health check that was produced.
177
+ *
178
+ * The COMPLETE check is always persisted to the {@link NotificationUser}'s `hc` field. What comes back
179
+ * here can be narrower: a `sendProbe` call is about one delivery method's test message, so unless
180
+ * `returnFullHealthCheck` was set its result carries only the methods that were actually probed, with
181
+ * `s` rolled up over just those and the account-wide findings left out. Read the document for the
182
+ * whole picture — see {@link NotificationUserHealthCheckParams.returnFullHealthCheck}.
183
+ */
184
+ readonly healthCheck: NotificationHealthCheck;
185
+ /**
186
+ * The number of probes dispatched by this run.
187
+ */
188
+ readonly probesDispatched: number;
189
+ /**
190
+ * The number of previously-pending probes this run resolved to a final status.
191
+ */
192
+ readonly probesResolved: number;
193
+ }
98
194
  export interface ResyncAllNotificationUserParams {
99
195
  }
100
196
  export declare const resyncAllNotificationUserParamsType: Type<ResyncAllNotificationUserParams>;
@@ -310,6 +406,9 @@ export type NotificationBoxModelCrudFunctionsConfig = {
310
406
  _: UpdateNotificationUserParams;
311
407
  resync: [ResyncNotificationUserParams, ResyncNotificationUserResult];
312
408
  };
409
+ invoke: {
410
+ healthCheck: [NotificationUserHealthCheckParams, NotificationUserHealthCheckResult];
411
+ };
313
412
  };
314
413
  readonly notificationSummary: {
315
414
  update: {
@@ -344,6 +443,9 @@ export declare abstract class NotificationFunctions implements ModelFirebaseFunc
344
443
  update: ModelFirebaseCrudFunction<UpdateNotificationUserParams>;
345
444
  resync: ModelFirebaseCrudFunction<ResyncNotificationUserParams, ResyncNotificationUserResult>;
346
445
  };
446
+ invokeNotificationUser: {
447
+ healthCheck: ModelFirebaseCrudFunction<NotificationUserHealthCheckParams, NotificationUserHealthCheckResult>;
448
+ };
347
449
  };
348
450
  abstract notificationSummary: {
349
451
  updateNotificationSummary: {
@@ -40,3 +40,18 @@ export declare const NOTIFICATION_USER_BLOCKED_FROM_BEING_ADD_TO_RECIPIENTS_ERRO
40
40
  * Thrown when attempting to update a recipient config that is locked by the user.
41
41
  */
42
42
  export declare const NOTIFICATION_USER_LOCKED_CONFIG_FROM_BEING_UPDATED_ERROR_CODE = "NOTIFICATION_USER_LOCKED_CONFIG_FROM_BEING_UPDATED";
43
+ /**
44
+ * Thrown when a NotificationUser health check is run again before its throttle window has passed.
45
+ */
46
+ export declare const NOTIFICATION_USER_HEALTH_CHECK_THROTTLED_ERROR_CODE = "NOTIFICATION_USER_HEALTH_CHECK_THROTTLED";
47
+ /**
48
+ * Thrown when a NotificationUser health check test message is dispatched again before its throttle
49
+ * window has passed. Tracked separately from the run throttle, since a probe sends a real message.
50
+ */
51
+ export declare const NOTIFICATION_USER_HEALTH_CHECK_PROBE_THROTTLED_ERROR_CODE = "NOTIFICATION_USER_HEALTH_CHECK_PROBE_THROTTLED";
52
+ /**
53
+ * Thrown when a NotificationUser health check's pending probes are verified again before its throttle
54
+ * window has passed. Tracked separately from the run throttle: verifying an in-flight test message is
55
+ * cheap and meant to be polled, so it neither answers to nor consumes the user's run allowance.
56
+ */
57
+ export declare const NOTIFICATION_USER_HEALTH_CHECK_VERIFY_THROTTLED_ERROR_CODE = "NOTIFICATION_USER_HEALTH_CHECK_VERIFY_THROTTLED";
@@ -22,6 +22,7 @@ import { type NotificationBoxRecipient, type NotificationRecipientWithConfig, ty
22
22
  import { type YearWeekCode } from '@dereekb/date';
23
23
  import { type UserRelatedById, type UserRelated } from '../user';
24
24
  import { AbstractFirestoreDocument, AbstractFirestoreDocumentWithParent, type CollectionGroup, type CollectionReference, type FirestoreCollection, type FirestoreCollectionGroup, type FirestoreCollectionWithParent, type FirestoreContext, type FirestoreModelKey, type SavedToFirestoreIfTrue, type SavedToFirestoreIfFalse, type PagedItemConverter, type PagedItemFirestoreCollection, type PagedItemPageData } from '../../common';
25
+ import { type NotificationHealthCheck } from './notification.healthcheck';
25
26
  import { type NotificationItem } from './notification.item';
26
27
  /**
27
28
  * Abstract class providing access to all notification-related Firestore collections.
@@ -142,6 +143,16 @@ export interface NotificationUser extends UserRelated, UserRelatedById {
142
143
  * @dbxModelVariable needsConfigSync
143
144
  */
144
145
  ns?: Maybe<NeedsSyncBoolean>;
146
+ /**
147
+ * The result of the most recent notification delivery health check run for this user.
148
+ *
149
+ * Persisted rather than returned-and-forgotten because delivery confirmation is asynchronous:
150
+ * a probe dispatched by one health check run is resolved by a later one. Absent until a check
151
+ * has been run at least once.
152
+ *
153
+ * @dbxModelVariable healthCheck
154
+ */
155
+ hc?: Maybe<NotificationHealthCheck>;
145
156
  }
146
157
  export type NotificationUserRoles = 'sync' | GrantedUpdateRole | GrantedReadRole;
147
158
  export declare class NotificationUserDocument extends AbstractFirestoreDocument<NotificationUser, NotificationUserDocument, typeof notificationUserIdentity> {