@dereekb/firebase-server 13.41.0 → 13.42.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.
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/model",
3
- "version": "13.41.0",
3
+ "version": "13.42.0",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
- "@dereekb/analytics": "13.41.0",
7
- "@dereekb/date": "13.41.0",
8
- "@dereekb/firebase": "13.41.0",
9
- "@dereekb/firebase-server": "13.41.0",
10
- "@dereekb/model": "13.41.0",
11
- "@dereekb/nestjs": "13.41.0",
12
- "@dereekb/rxjs": "13.41.0",
13
- "@dereekb/util": "13.41.0",
6
+ "@dereekb/analytics": "13.42.0",
7
+ "@dereekb/date": "13.42.0",
8
+ "@dereekb/firebase": "13.42.0",
9
+ "@dereekb/firebase-server": "13.42.0",
10
+ "@dereekb/model": "13.42.0",
11
+ "@dereekb/nestjs": "13.42.0",
12
+ "@dereekb/rxjs": "13.42.0",
13
+ "@dereekb/util": "13.42.0",
14
14
  "@nestjs/common": "^11.1.19",
15
15
  "@nestjs/config": "^4.0.4",
16
16
  "archiver": "^7.0.1",
@@ -0,0 +1,121 @@
1
+ import { type AppCalendarTypeConfigServiceRef, type CalendarDocument, type CalendarFirestoreCollections, type FirestoreContextReference, type FlagStaleCalendarsForSyncParams, type FlagStaleCalendarsForSyncResult, type RotateCalendarIcsParams, type RotateCalendarIcsResult, type StorageFileFirestoreCollections, type SyncAllFlaggedCalendarsParams, type SyncAllFlaggedCalendarsResult, type SyncCalendarParams, type SyncCalendarResult } from '@dereekb/firebase';
2
+ import { type FirebaseServerActionsContext, type FirebaseServerStorageServiceRef } from '@dereekb/firebase-server';
3
+ import { type TransformAndValidateFunctionResult } from '@dereekb/model';
4
+ import { type InjectionToken } from '@nestjs/common';
5
+ import { type StorageFileServerActions } from '../storagefile/storagefile.action.server';
6
+ /**
7
+ * NestJS injection token for the {@link BaseCalendarServerActionsContext}.
8
+ */
9
+ export declare const BASE_CALENDAR_SERVER_ACTION_CONTEXT_TOKEN: InjectionToken;
10
+ /**
11
+ * NestJS injection token for the fully assembled {@link CalendarServerActionsContext}.
12
+ */
13
+ export declare const CALENDAR_SERVER_ACTION_CONTEXT_TOKEN: InjectionToken;
14
+ /**
15
+ * Minimal context providing the Firebase infrastructure, storage and Firestore collections every Calendar
16
+ * server action needs.
17
+ */
18
+ export interface BaseCalendarServerActionsContext extends FirebaseServerActionsContext, CalendarFirestoreCollections, StorageFileFirestoreCollections, FirebaseServerStorageServiceRef, FirestoreContextReference {
19
+ }
20
+ /**
21
+ * Full context for the Calendar server actions, adding the type registry and the StorageFile actions the
22
+ * sweep re-flags through.
23
+ */
24
+ export interface CalendarServerActionsContext extends BaseCalendarServerActionsContext, AppCalendarTypeConfigServiceRef {
25
+ readonly storageFileServerActions: StorageFileServerActions;
26
+ }
27
+ /**
28
+ * The publish-side server actions for the Calendar model.
29
+ *
30
+ * PUBLISH-ONLY BY DESIGN. There is no `upsertCalendarEvents` / `removeCalendarEvents` action: a caller
31
+ * already holds a transaction and an accessor when it decides to touch a calendar, so an action that opened
32
+ * its own transaction would either fight the caller's or force an awkward split write. Callers merge the
33
+ * `calendar.util.ts` templates into their own write instead, and those templates carry the `s: true`
34
+ * invariant that makes this sweep correct.
35
+ *
36
+ * {@link CalendarServerActions.rotateCalendarIcs} is the one action with a callable surface
37
+ * (`calendar/update/rotateIcs`), since revoking a published feed url has to be reachable by its owner. It
38
+ * enforces no permission of its own — the callable's role gate is the authorization.
39
+ *
40
+ * @see {@link calendarServerActions} for the concrete implementation factory.
41
+ */
42
+ export declare abstract class CalendarServerActions {
43
+ abstract syncCalendar(params: SyncCalendarParams): Promise<TransformAndValidateFunctionResult<SyncCalendarParams, (calendarDocument: CalendarDocument) => Promise<SyncCalendarResult>>>;
44
+ abstract rotateCalendarIcs(params: RotateCalendarIcsParams): Promise<TransformAndValidateFunctionResult<RotateCalendarIcsParams, (calendarDocument: CalendarDocument) => Promise<RotateCalendarIcsResult>>>;
45
+ abstract syncAllFlaggedCalendars(params: SyncAllFlaggedCalendarsParams): Promise<TransformAndValidateFunctionResult<SyncAllFlaggedCalendarsParams, () => Promise<SyncAllFlaggedCalendarsResult>>>;
46
+ abstract flagStaleCalendarsForSync(params: FlagStaleCalendarsForSyncParams): Promise<TransformAndValidateFunctionResult<FlagStaleCalendarsForSyncParams, () => Promise<FlagStaleCalendarsForSyncResult>>>;
47
+ }
48
+ /**
49
+ * Creates a concrete {@link CalendarServerActions} implementation from the given context.
50
+ *
51
+ * @param context - The fully assembled calendar server actions context.
52
+ * @returns The server actions.
53
+ */
54
+ export declare function calendarServerActions(context: CalendarServerActionsContext): CalendarServerActions;
55
+ /**
56
+ * Factory for the `syncCalendar` action, the Calendar counterpart of
57
+ * `regenerateStorageFileGroupContentFactory`.
58
+ *
59
+ * In ONE transaction it prunes the calendar, ensures its ICS StorageFile exists, and clears `s`. It does not
60
+ * write `sat` — that belongs to the processor's success path alone, which is what makes
61
+ * `s === false && sat < uat` mean "queued, not yet published" and lets `flagStaleCalendarsForSync()` heal a
62
+ * run that died in between.
63
+ *
64
+ * The re-flag of an EXISTING ICS StorageFile happens AFTER the transaction commits, through the public
65
+ * `processStorageFile` action rather than the by-convention-private in-transaction helper. That is safe
66
+ * precisely because of the invariant above: a lost re-flag self-heals within one resync interval instead of
67
+ * silently stranding the calendar.
68
+ *
69
+ * @param context - The calendar server actions context.
70
+ * @returns An async transform-and-validate function that syncs a single Calendar.
71
+ */
72
+ export declare function syncCalendarFactory(context: CalendarServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<SyncCalendarParams, (calendarDocument: CalendarDocument) => Promise<SyncCalendarResult>, object, unknown>;
73
+ /**
74
+ * Factory for the `rotateCalendarIcs` action: the REVOCATION primitive for a published feed url.
75
+ *
76
+ * A published feed url is a bearer credential — unguessable, but permanent until rotated, and stored by a
77
+ * subscriber (Google keeps it in the subscriber's account). Rotation is the only revocation available for a
78
+ * zero-auth feed, which is why it is a first-class action rather than a manual cleanup.
79
+ *
80
+ * It invents no new mechanism. `calendarIcsFileStoragePath()` keys the published object by the ICS
81
+ * StorageFile's OWN id, and `syncCalendarFactory` already mints a fresh StorageFile — new id, new path, new
82
+ * url — whenever the existing one is absent or QUEUED_FOR_DELETE. So in ONE transaction this simply:
83
+ *
84
+ * - flags the current ICS StorageFile for delete (the existing StorageFile delete machinery is what actually
85
+ * removes the old object, and therefore what actually revokes the old url)
86
+ * - clears `isf` and `iu`, and sets `s: true`
87
+ *
88
+ * `syncCalendar` is then called immediately after the transaction commits, so the replacement exists without
89
+ * waiting for the hourly sweep — and its publish is EXPEDITED (`runImmediately`) rather than left queued, so
90
+ * `iu` normally holds the new url by the time this returns.
91
+ *
92
+ * That expedite is the difference between a rotation and any other sync. Every other path can afford to let
93
+ * the sweep publish, because the calendar still has a working url in the meantime. A rotation does not: it
94
+ * has already revoked the old one, so until the replacement uploads the user has NO link at all. Result
95
+ * field `publishedIcs` reports whether the inline publish landed; the sweep remains the backstop when it
96
+ * did not.
97
+ *
98
+ * @param context - The calendar server actions context.
99
+ * @returns An async transform-and-validate function that rotates a single Calendar's ICS link.
100
+ */
101
+ export declare function rotateCalendarIcsFactory(context: CalendarServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<RotateCalendarIcsParams, (calendarDocument: CalendarDocument) => Promise<RotateCalendarIcsResult>, object, unknown>;
102
+ /**
103
+ * Factory for the `syncAllFlaggedCalendars` action, the Calendar counterpart of
104
+ * `regenerateAllFlaggedStorageFileGroupsContentFactory`.
105
+ *
106
+ * @param context - The calendar server actions context.
107
+ * @returns An async transform-and-validate function that sweeps every flagged Calendar.
108
+ */
109
+ export declare function syncAllFlaggedCalendarsFactory(context: CalendarServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<SyncAllFlaggedCalendarsParams, () => Promise<SyncAllFlaggedCalendarsResult>, object, unknown>;
110
+ /**
111
+ * Factory for the `flagStaleCalendarsForSync` action: the self-healing backstop.
112
+ *
113
+ * For every registered {@link CalendarType} it re-flags Calendars whose last successful publish predates that
114
+ * type's resync interval. That covers a sweep that cleared `s` but whose ICS never finished publishing, and
115
+ * it keeps an `expand`-mode calendar from sliding off the end of its expansion window — with no extra field
116
+ * and no extra mechanism.
117
+ *
118
+ * @param context - The calendar server actions context.
119
+ * @returns An async transform-and-validate function that re-flags stale Calendars.
120
+ */
121
+ export declare function flagStaleCalendarsForSyncFactory(context: CalendarServerActionsContext): import("@dereekb/model").TransformAndValidateFunctionResultFunction<FlagStaleCalendarsForSyncParams, () => Promise<FlagStaleCalendarsForSyncResult>, object, unknown>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Creates an error indicating the Calendar module was configured without an ICS domain.
3
+ *
4
+ * @returns An internal-server HttpsError with the CALENDAR_ICS_DOMAIN_NOT_CONFIGURED error code.
5
+ */
6
+ export declare function calendarIcsDomainNotConfiguredError(): import("firebase-functions/https").HttpsError;
7
+ /**
8
+ * Creates an error indicating the Calendar's ICS StorageFile could not be resolved or created.
9
+ *
10
+ * @returns A precondition-conflict HttpsError with the CALENDAR_ICS_STORAGE_FILE_UNAVAILABLE error code.
11
+ */
12
+ export declare function calendarIcsStorageFileUnavailableError(): import("firebase-functions/https").HttpsError;
13
+ /**
14
+ * Creates an error indicating the Calendar's ICS link was rotated again before its throttle window passed.
15
+ *
16
+ * Thrown by the rotate action when the calendar's stored rotation instant is too recent. The client derives
17
+ * the same window from the same field, so reaching this error means the caller bypassed the UI.
18
+ *
19
+ * @param nextRotateAt - The time the next rotation is allowed.
20
+ * @returns A precondition-conflict HttpsError with the CALENDAR_ICS_ROTATE_THROTTLED error code.
21
+ */
22
+ export declare function calendarIcsRotateThrottledError(nextRotateAt: Date): import("firebase-functions/https").HttpsError;
@@ -0,0 +1,72 @@
1
+ import { type InjectionToken, type ModuleMetadata } from '@nestjs/common';
2
+ import { type Maybe } from '@dereekb/util';
3
+ import { AppCalendarTypeConfigService, type CalendarTypeConfig } from '@dereekb/firebase';
4
+ import { type BaseCalendarServerActionsContext, CalendarServerActions, type CalendarServerActionsContext } from './calendar.action.server';
5
+ import { StorageFileServerActions } from '../storagefile/storagefile.action.server';
6
+ /**
7
+ * NestJS injection token for the app's `CalendarTypeConfig[]` registry.
8
+ *
9
+ * Apps bind this token via {@link appCalendarModuleMetadata} by passing `calendarTypeConfigs`.
10
+ */
11
+ export declare const CALENDAR_TYPE_CONFIGS_TOKEN: InjectionToken;
12
+ /**
13
+ * NestJS injection token for the domain every generated event UID is suffixed with.
14
+ */
15
+ export declare const CALENDAR_ICS_DOMAIN_TOKEN: InjectionToken;
16
+ /**
17
+ * Factory that builds the app's {@link AppCalendarTypeConfigService} from its registered configs.
18
+ *
19
+ * @param calendarTypeConfigs - The app's calendar type registry.
20
+ * @returns The service.
21
+ */
22
+ export declare function appCalendarTypeConfigServiceFactory(calendarTypeConfigs: CalendarTypeConfig[]): AppCalendarTypeConfigService;
23
+ /**
24
+ * Factory that assembles the full {@link CalendarServerActionsContext}.
25
+ *
26
+ * @param context - The base context providing Firebase infrastructure and collections.
27
+ * @param appCalendarTypeConfigServiceInstance - The app's calendar type registry service.
28
+ * @param storageFileServerActions - The StorageFile actions the sweep re-flags through.
29
+ * @returns The fully assembled context.
30
+ */
31
+ export declare function calendarServerActionsContextFactory(context: BaseCalendarServerActionsContext, appCalendarTypeConfigServiceInstance: AppCalendarTypeConfigService, storageFileServerActions: StorageFileServerActions): CalendarServerActionsContext;
32
+ /**
33
+ * Factory that creates a {@link CalendarServerActions} instance from the assembled context.
34
+ *
35
+ * @param context - The fully assembled calendar server actions context.
36
+ * @returns The server actions.
37
+ */
38
+ export declare function calendarServerActionsFactory(context: CalendarServerActionsContext): CalendarServerActions;
39
+ export interface ProvideAppCalendarMetadataConfig extends Pick<ModuleMetadata, 'imports' | 'exports' | 'providers'> {
40
+ /**
41
+ * The AppCalendarModule requires the following dependencies in order to initialize properly:
42
+ * - BaseCalendarServerActionsContext (BASE_CALENDAR_SERVER_ACTION_CONTEXT_TOKEN)
43
+ * - StorageFileServerActions
44
+ *
45
+ * This module declaration makes it easier to import a module that exports those dependencies.
46
+ */
47
+ readonly dependencyModule?: Maybe<Required<ModuleMetadata>['imports']['0']>;
48
+ /**
49
+ * The app's {@link CalendarType} registry. Bound to {@link CALENDAR_TYPE_CONFIGS_TOKEN}.
50
+ */
51
+ readonly calendarTypeConfigs: CalendarTypeConfig[];
52
+ /**
53
+ * The domain every generated event UID is suffixed with. I.E. "example.com".
54
+ *
55
+ * REQUIRED: the UID factory deliberately has no random fallback, since a UID that changes between
56
+ * publishes makes every client create a duplicate event rather than update the one it holds.
57
+ */
58
+ readonly icsDomain: string;
59
+ }
60
+ /**
61
+ * Convenience function used to generate ModuleMetadata for an app's CalendarModule.
62
+ *
63
+ * By default this module exports:
64
+ * - CalendarServerActionsContext (CALENDAR_SERVER_ACTION_CONTEXT_TOKEN)
65
+ * - CalendarServerActions
66
+ * - AppCalendarTypeConfigService
67
+ * - CALENDAR_ICS_DOMAIN_TOKEN
68
+ *
69
+ * @param config - The module configuration.
70
+ * @returns The assembled {@link ModuleMetadata} for the calendar module.
71
+ */
72
+ export declare function appCalendarModuleMetadata(config: ProvideAppCalendarMetadataConfig): ModuleMetadata;
@@ -0,0 +1,52 @@
1
+ import { type AppCalendarTypeConfigService, type CalendarFirestoreCollections, type CalendarIcsStorageFileProcessingSubtask, type CalendarIcsStorageFileProcessingSubtaskMetadata, type FirebaseStorageAccessor } from '@dereekb/firebase';
2
+ import { type StorageFileProcessingPurposeSubtaskProcessorConfigWithTarget } from '../storagefile/storagefile.task.service.handler';
3
+ /**
4
+ * Cache-Control set on the published ICS object.
5
+ *
6
+ * A public GCS object otherwise defaults to `public, max-age=3600`, which lets a freshly regenerated feed be
7
+ * served stale from the edge for an hour on top of the subscriber's own polling lag. `Last-Modified` / `ETag`
8
+ * need no work here — GCS derives and serves them, and answers `304` to conditional GETs, automatically.
9
+ */
10
+ export declare const CALENDAR_ICS_PUBLISHED_CACHE_CONTROL = "public, max-age=300, stale-while-revalidate=60";
11
+ /**
12
+ * Content-Disposition set on the published ICS object.
13
+ *
14
+ * `attachment` because the STORED disposition is now the only one a downloader gets: the download button
15
+ * derives the public url client-side, and a public url — unlike a signed one, whose `responseDisposition`
16
+ * the button used to set per-request — carries no override. Without this a browser hands `text/calendar`
17
+ * straight to the OS calendar app rather than saving it.
18
+ *
19
+ * Harmless for the subscribe channel, which was the reason this was `inline`: a calendar client fetches the
20
+ * feed programmatically and ignores `Content-Disposition` entirely.
21
+ */
22
+ export declare const CALENDAR_ICS_PUBLISHED_CONTENT_DISPOSITION = "attachment; filename=\"calendar.ics\"";
23
+ /**
24
+ * Configuration for {@link calendarIcsStorageFileProcessingPurposeSubtaskProcessor}.
25
+ */
26
+ export interface CalendarIcsStorageFileProcessingPurposeSubtaskProcessorConfig {
27
+ readonly calendarFirestoreCollections: CalendarFirestoreCollections;
28
+ readonly storageAccessor: FirebaseStorageAccessor;
29
+ readonly appCalendarTypeConfigService: AppCalendarTypeConfigService;
30
+ /**
31
+ * The domain every generated event UID is suffixed with.
32
+ */
33
+ readonly icsDomain: string;
34
+ }
35
+ /**
36
+ * Creates the ICS subtask processor for Calendar publishing.
37
+ *
38
+ * Far shorter than the StorageFileGroup zip processor, and deliberately so: there is no stream branch,
39
+ * because `FirebaseStorageAccessorFile.upload()` is non-optional while `uploadStream?` is not, and an ICS for
40
+ * a few hundred events is tens of kilobytes.
41
+ *
42
+ * It renders with `now: calendar.uat` rather than the wall clock, so DTSTAMP moves only when the content
43
+ * moves — which is what makes the output byte-identical for identical input. The processor is idempotent: a
44
+ * retry simply re-renders and re-uploads.
45
+ *
46
+ * The shared cleanup step then writes `ps: SUCCESS`, `pcat: now`, `pn: null`, which is exactly "the published
47
+ * ICS is uploaded and current".
48
+ *
49
+ * @param config - The collections, storage accessor, type registry, and UID domain.
50
+ * @returns A subtask processor config targeting the Calendar ICS purpose.
51
+ */
52
+ export declare function calendarIcsStorageFileProcessingPurposeSubtaskProcessor(config: CalendarIcsStorageFileProcessingPurposeSubtaskProcessorConfig): StorageFileProcessingPurposeSubtaskProcessorConfigWithTarget<CalendarIcsStorageFileProcessingSubtaskMetadata, CalendarIcsStorageFileProcessingSubtask>;
@@ -0,0 +1,4 @@
1
+ export * from './calendar.action.server';
2
+ export * from './calendar.error';
3
+ export * from './calendar.module';
4
+ export * from './calendar.task.service.handler';
@@ -1,3 +1,4 @@
1
+ export * from './calendar';
1
2
  export * from './mailgun';
2
3
  export * from './notification';
3
4
  export * from './storagefile';
@@ -1,2 +1,3 @@
1
1
  export * from './notification.healthcheck.mailgun';
2
2
  export * from './notification.send.service.mailgun';
3
+ export * from './notification.send.service.mailgun.attachment';
@@ -0,0 +1,40 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type MailgunFileAttachment } from '@dereekb/nestjs/mailgun';
3
+ import { type NotificationMessageCalendarAttachment, type NotificationMessageCalendarAttachmentFactoryInput } from '@dereekb/firebase';
4
+ /**
5
+ * @module notification.send.service.mailgun.attachment
6
+ *
7
+ * Bridges a notification message's iTIP calendar payload onto a Mailgun request as a calendar MIME part.
8
+ *
9
+ * The payload cannot ride a batched `to[]`: attachments live on the REQUEST, a `MailgunRecipient` has no
10
+ * per-recipient attachment slot, and a `METHOD:REQUEST` invite is only rendered inline by a client that
11
+ * finds its OWN address in the payload's ATTENDEE. A builder must therefore give a recipient with a payload
12
+ * a request of its own -- either by emitting one directly, or by setting the result on that recipient's
13
+ * `MailgunRecipientBatchSendTarget.attachments` and letting
14
+ * `expandMailgunRecipientBatchSendTargetRequestFactory()` expand it while the rest still batch.
15
+ */
16
+ /**
17
+ * Converts a rendered iTIP calendar payload into the Mailgun attachment that carries it.
18
+ *
19
+ * The `contentType` is the whole point: a calendar part typed `text/calendar; method=REQUEST; charset=utf-8`
20
+ * is auto-processed as an invitation by Gmail, Outlook and Apple Mail, while the same bytes under Mailgun's
21
+ * default type render as an ordinary paperclip.
22
+ *
23
+ * @param calendarAttachment - The rendered payload.
24
+ * @returns The Mailgun attachment.
25
+ *
26
+ * @__NO_SIDE_EFFECTS__
27
+ */
28
+ export declare function notificationMessageCalendarAttachmentToMailgunFileAttachment(calendarAttachment: NotificationMessageCalendarAttachment): MailgunFileAttachment;
29
+ /**
30
+ * Renders the message's calendar payload for one recipient and converts it to a Mailgun attachment.
31
+ *
32
+ * The single entry point a template builder needs: it reads the message's `calendarAttachmentFactory`,
33
+ * invokes it for the recipient, and returns `undefined` when the message carries no factory or the factory
34
+ * declines this recipient. That is the signal to batch the recipient normally rather than give it a request
35
+ * of its own; a non-undefined return belongs on that recipient's `MailgunRecipientBatchSendTarget.attachments`.
36
+ *
37
+ * @param input - The message and the address the payload's ATTENDEE must name.
38
+ * @returns The Mailgun attachment, or `undefined` when this recipient gets no calendar part.
39
+ */
40
+ export declare function mailgunCalendarFileAttachmentForNotificationMessage(input: NotificationMessageCalendarAttachmentFactoryInput): Promise<Maybe<MailgunFileAttachment>>;
@@ -1,5 +1,6 @@
1
1
  import { type StorageFilePurposeUploadPolicy } from '@dereekb/firebase';
2
2
  import { type McpToolDetailsBuilder } from '@dereekb/firebase-server';
3
+ import { type Maybe } from '@dereekb/util';
3
4
  /**
4
5
  * Config for {@link storageFileCreateSignedUploadUrlToolDetailsFactory}.
5
6
  */
@@ -30,3 +31,40 @@ export interface StorageFileCreateSignedUploadUrlToolDetailsFactoryConfig {
30
31
  * @__NO_SIDE_EFFECTS__
31
32
  */
32
33
  export declare function storageFileCreateSignedUploadUrlToolDetailsFactory(config: StorageFileCreateSignedUploadUrlToolDetailsFactoryConfig): McpToolDetailsBuilder;
34
+ /**
35
+ * Config for {@link storageFileProcessToolDetailsFactory}.
36
+ */
37
+ export interface StorageFileProcessToolDetailsFactoryConfig {
38
+ /**
39
+ * App-specific guidance appended to the generated description.
40
+ *
41
+ * Use this to describe what processing means for the app's own purposes — for example which
42
+ * purposes run a validation flow, and where that flow records its verdict.
43
+ */
44
+ readonly additionalGuidance?: Maybe<string>;
45
+ }
46
+ /**
47
+ * Builds the {@link McpToolDetailsBuilder} that customizes the MCP tool description for the
48
+ * `storageFileProcess` update-function.
49
+ *
50
+ * The generated description spells out which flag each processing state requires. The default
51
+ * schema-derived description cannot convey that a file whose processor ran to completion sits in
52
+ * the SUCCESS state even when the outcome was a rejection, so a caller re-validating a rejected
53
+ * file would otherwise reach for the flagless call and get an "already processed" error.
54
+ *
55
+ * The factory captures the description once at wiring time; the returned builder is a pure
56
+ * synchronous function called by the framework on every `tools/list` request.
57
+ *
58
+ * @param config - Optional factory config carrying app-specific guidance to append.
59
+ * @returns A builder that emits the state-aware tool description and leaves the input schema at its default.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const toolDetails = storageFileProcessToolDetailsFactory({
64
+ * additionalGuidance: 'The "resume" purpose records its verdict on the parent Profile.'
65
+ * });
66
+ * ```
67
+ *
68
+ * @__NO_SIDE_EFFECTS__
69
+ */
70
+ export declare function storageFileProcessToolDetailsFactory(config?: Maybe<StorageFileProcessToolDetailsFactoryConfig>): McpToolDetailsBuilder;
@@ -1,2 +1,3 @@
1
1
  export * from './system.private';
2
2
  export * from './system.private.module';
3
+ export * from './system.scheduler';
@@ -0,0 +1,144 @@
1
+ import { type FirestoreDocument, type SchedulerSystemStateRead, type SystemState, type SystemStateFirestoreCollectionLike, type SystemStateStoredData } from '@dereekb/firebase';
2
+ import { type Getter, type Hours, type Maybe } from '@dereekb/util';
3
+ /**
4
+ * Configuration for a single gate evaluation.
5
+ */
6
+ export interface SchedulerSystemStateGateConfig {
7
+ /**
8
+ * Run every Nth hour of the day.
9
+ *
10
+ * Matched by modulo against the hour-of-day, so only divisors of 24 divide the day evenly. See
11
+ * `isNthHourOfDay()` in `@dereekb/firebase`.
12
+ */
13
+ readonly everyNHours: Hours;
14
+ }
15
+ /**
16
+ * The outcome of a {@link SchedulerSystemStateAccessor.checkAndClaim} call.
17
+ *
18
+ * Extends {@link SchedulerSystemStateRead}, so past the pass/fail answer it is also the read the
19
+ * decision was made from: the same `now`, the same hour-of-day, and the same bound predicates. That
20
+ * is what lets a single hourly claim fan out into per-task sub-gates without a second read or a
21
+ * second clock —
22
+ *
23
+ * ```ts
24
+ * const gate = await schedulerSystemState.checkAndClaim({ everyNHours: 1 });
25
+ *
26
+ * if (!gate.claimed) {
27
+ * return;
28
+ * }
29
+ *
30
+ * await hourlyWork();
31
+ *
32
+ * if (gate.isNthHourOfDay(3)) {
33
+ * await everyThreeHoursWork();
34
+ * }
35
+ * ```
36
+ *
37
+ * NOTE: this is an object, so it is ALWAYS truthy. `if (await checkAndClaim(...))` always passes —
38
+ * branch on {@link SchedulerSystemStateClaim.claimed}.
39
+ *
40
+ * The inherited {@link SchedulerSystemStateRead} members describe the state as it was read BEFORE
41
+ * the claim was stamped, deliberately: `lastRunAt` is the previous claim, `hasRunInCurrentHour` is
42
+ * false on a successful claim, and `isOpen()` still answers for the other intervals this hour rather
43
+ * than reporting closed against the claim this very call just wrote.
44
+ */
45
+ export interface SchedulerSystemStateClaim extends SchedulerSystemStateRead {
46
+ /**
47
+ * The interval the claim was evaluated for.
48
+ */
49
+ readonly everyNHours: Hours;
50
+ /**
51
+ * Whether THIS call claimed the hour, and so whether the caller may run its work.
52
+ */
53
+ readonly claimed: boolean;
54
+ /**
55
+ * The moment stamped as the new `lat`, or null when the gate was closed and nothing was written.
56
+ *
57
+ * Always equal to {@link SchedulerSystemStateRead.now} when {@link claimed} is true.
58
+ */
59
+ readonly claimedAt: Maybe<Date>;
60
+ }
61
+ /**
62
+ * Reads and claims the scheduler's hourly run gate.
63
+ */
64
+ export interface SchedulerSystemStateAccessor {
65
+ /**
66
+ * Reads the gate state without claiming it.
67
+ *
68
+ * @returns The state, which can be evaluated against any number of intervals.
69
+ */
70
+ read(): Promise<SchedulerSystemStateRead>;
71
+ /**
72
+ * Reads, evaluates the gate for the given interval, and CLAIMS the hour, in one transaction.
73
+ *
74
+ * The claim is stamped BEFORE this resolves, deliberately: a crash or a function timeout in the
75
+ * caller's work must still cost the whole window. Otherwise the hourly cron degrades into an
76
+ * hourly retry loop against work that is already failing.
77
+ *
78
+ * Remember that one gate is one `lat`. Two callers with different `everyNHours` sharing the
79
+ * document will have whichever one passes first claim the hour for BOTH — the second gets
80
+ * `claimed: false` even if its own interval matched. That is the intended semantics of a single
81
+ * gate, but it is the thing a future second caller will trip over. Prefer claiming ONCE at the top
82
+ * of the schedule function and sub-gating the individual tasks off the returned
83
+ * {@link SchedulerSystemStateClaim}.
84
+ *
85
+ * @param config - The interval to evaluate.
86
+ * @returns The claim outcome, which also carries the read it was decided from.
87
+ */
88
+ checkAndClaim(config: SchedulerSystemStateGateConfig): Promise<SchedulerSystemStateClaim>;
89
+ }
90
+ /**
91
+ * Configuration for {@link schedulerSystemStateAccessorFactory}.
92
+ */
93
+ export interface SchedulerSystemStateAccessorFactoryConfig {
94
+ /**
95
+ * Clock used to evaluate the gate and to stamp `lat`. Defaults to the current time.
96
+ *
97
+ * Overriding it is what lets a test place "now" at a specific hour-of-day — the gate is a
98
+ * modulo against the hour, so there is otherwise no way to exercise a non-matching hour without
99
+ * waiting for one.
100
+ */
101
+ readonly nowFactory?: Maybe<Getter<Date>>;
102
+ }
103
+ /**
104
+ * Creates a {@link SchedulerSystemStateAccessor} for a SystemState collection.
105
+ */
106
+ export type SchedulerSystemStateAccessorFactory = <D extends FirestoreDocument<SystemState<SystemStateStoredData>>>(systemStateCollection: SystemStateFirestoreCollectionLike<SystemStateStoredData, D>) => SchedulerSystemStateAccessor;
107
+ /**
108
+ * Creates a {@link SchedulerSystemStateAccessorFactory}, the Firestore-backed half of the scheduler's
109
+ * hourly run gate.
110
+ *
111
+ * The gate answers "should the scheduler run its Nth-hour body during this hour?" for the app as a
112
+ * whole, off the single `lat` on the `sys/scheduler` document. Claim it ONCE at the top of a
113
+ * schedule function; the individual tasks it guards carry no throttle of their own and sub-gate off
114
+ * the returned {@link SchedulerSystemStateClaim} instead.
115
+ *
116
+ * The collection MUST have `schedulerSystemDataConverter` registered under
117
+ * {@link SCHEDULER_SYSTEM_STATE_TYPE}. Without it the collection falls back to the pass-through
118
+ * converter and `lat` reads back as a raw Firestore `Timestamp`, which no hour comparison can match —
119
+ * so the gate would silently open on every call. {@link SchedulerSystemStateAccessor.read} and
120
+ * `checkAndClaim` throw on that rather than let it through.
121
+ *
122
+ * @param config - The clock override, if any.
123
+ * @returns A factory producing an accessor over a given SystemState collection.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const schedulerSystemState = schedulerSystemStateAccessorFactory()(systemStateCollection);
128
+ *
129
+ * export const hourlySchedule: MyScheduleFunction = async (request) => {
130
+ * const gate = await schedulerSystemState.checkAndClaim({ everyNHours: 1 });
131
+ *
132
+ * if (!gate.claimed) {
133
+ * return;
134
+ * }
135
+ *
136
+ * await hourlyWork();
137
+ *
138
+ * if (gate.isNthHourOfDay(3)) {
139
+ * await everyThreeHoursWork();
140
+ * }
141
+ * };
142
+ * ```
143
+ */
144
+ export declare function schedulerSystemStateAccessorFactory(config?: Maybe<SchedulerSystemStateAccessorFactoryConfig>): SchedulerSystemStateAccessorFactory;
package/oidc/index.esm.js CHANGED
@@ -3625,7 +3625,7 @@ function _unsupported_iterable_to_array$5(o, minLen) {
3625
3625
  * @returns The union of config-level and profile-level admin-only scopes.
3626
3626
  */ function adminOnlyScopesForOidcProviderConfig(providerConfig) {
3627
3627
  var _providerConfig_adminOnlyScopes, _providerConfig_providerProfiles;
3628
- return new Set(_to_consumable_array$5((_providerConfig_adminOnlyScopes = providerConfig.adminOnlyScopes) !== null && _providerConfig_adminOnlyScopes !== void 0 ? _providerConfig_adminOnlyScopes : []).concat(_to_consumable_array$5(adminOnlyScopesForOidcProviderProfiles((_providerConfig_providerProfiles = providerConfig.providerProfiles) !== null && _providerConfig_providerProfiles !== void 0 ? _providerConfig_providerProfiles : []))));
3628
+ return new Set(_to_consumable_array$5((_providerConfig_adminOnlyScopes = providerConfig.adminOnlyScopes) !== null && _providerConfig_adminOnlyScopes !== void 0 ? _providerConfig_adminOnlyScopes : []).concat(_to_consumable_array$5(Array.from(adminOnlyScopesForOidcProviderProfiles((_providerConfig_providerProfiles = providerConfig.providerProfiles) !== null && _providerConfig_providerProfiles !== void 0 ? _providerConfig_providerProfiles : [])))));
3629
3629
  }
3630
3630
 
3631
3631
  /**
@@ -6571,7 +6571,7 @@ var OidcInteractionController_1;
6571
6571
  effectiveOIDCScopes = resolveEffectiveSubset({
6572
6572
  missing: missingOIDCScope,
6573
6573
  requestedSubset: body.grantedOIDCScopes,
6574
- alwaysGranted: _to_consumable_array$1(ALWAYS_GRANTED_OIDC_SCOPES).concat(_to_consumable_array$1(clientRequiredScopes)),
6574
+ alwaysGranted: _to_consumable_array$1(ALWAYS_GRANTED_OIDC_SCOPES).concat(_to_consumable_array$1(Array.from(clientRequiredScopes))),
6575
6575
  alreadyEncountered: encounteredOIDCScopes
6576
6576
  });
6577
6577
  // Scopes the existing Grant already holds (granted, minus any it rejected). Nothing here revokes
package/oidc/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/oidc",
3
- "version": "13.41.0",
3
+ "version": "13.42.0",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
- "@dereekb/analytics": "13.41.0",
7
- "@dereekb/date": "13.41.0",
8
- "@dereekb/firebase": "13.41.0",
9
- "@dereekb/firebase-server": "13.41.0",
10
- "@dereekb/model": "13.41.0",
11
- "@dereekb/nestjs": "13.41.0",
12
- "@dereekb/rxjs": "13.41.0",
13
- "@dereekb/util": "13.41.0",
14
- "@dereekb/zoho": "13.41.0",
6
+ "@dereekb/analytics": "13.42.0",
7
+ "@dereekb/date": "13.42.0",
8
+ "@dereekb/firebase": "13.42.0",
9
+ "@dereekb/firebase-server": "13.42.0",
10
+ "@dereekb/model": "13.42.0",
11
+ "@dereekb/nestjs": "13.42.0",
12
+ "@dereekb/rxjs": "13.42.0",
13
+ "@dereekb/util": "13.42.0",
14
+ "@dereekb/zoho": "13.42.0",
15
15
  "@nestjs/common": "^11.1.19",
16
16
  "@nestjs/config": "^4.0.4",
17
17
  "express": "^5.2.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server",
3
- "version": "13.41.0",
3
+ "version": "13.42.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -58,17 +58,17 @@
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@cantoo/pdf-lib": "^2.6.5",
61
- "@dereekb/analytics": "13.41.0",
62
- "@dereekb/calcom": "13.41.0",
63
- "@dereekb/date": "13.41.0",
64
- "@dereekb/dbx-core": "13.41.0",
65
- "@dereekb/discord": "13.41.0",
66
- "@dereekb/firebase": "13.41.0",
67
- "@dereekb/model": "13.41.0",
68
- "@dereekb/nestjs": "13.41.0",
69
- "@dereekb/rxjs": "13.41.0",
70
- "@dereekb/util": "13.41.0",
71
- "@dereekb/zoho": "13.41.0",
61
+ "@dereekb/analytics": "13.42.0",
62
+ "@dereekb/calcom": "13.42.0",
63
+ "@dereekb/date": "13.42.0",
64
+ "@dereekb/dbx-core": "13.42.0",
65
+ "@dereekb/discord": "13.42.0",
66
+ "@dereekb/firebase": "13.42.0",
67
+ "@dereekb/model": "13.42.0",
68
+ "@dereekb/nestjs": "13.42.0",
69
+ "@dereekb/rxjs": "13.42.0",
70
+ "@dereekb/util": "13.42.0",
71
+ "@dereekb/zoho": "13.42.0",
72
72
  "@google-cloud/firestore": "^7.11.6",
73
73
  "@google-cloud/storage": "^7.19.0",
74
74
  "@modelcontextprotocol/node": "2.0.0",