@rdlabo/workers-hono-kit 0.9.1 → 0.9.3

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/README.md CHANGED
@@ -79,6 +79,7 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
79
79
  | `normalizeTrailingSlash(request)` | Strip trailing slash(es) from the request URL before routing (Express/Nest parity). Does **not** 301-redirect — preserves POST/PUT/DELETE bodies. |
80
80
  | `HTTP_ERROR_PHRASES` | `{ 400, 401, 403, 404 }` → standard `error` field phrases. |
81
81
  | `createAuthMiddleware(options)` / `AuthMiddlewareOptions` | Factory for a Firebase-token auth middleware: reads the token header, verifies, resolves the DB user id, and stashes the result on the context. Omit `resolveUserId` for a token-only (login) guard. |
82
+ | `createIdentityAuthFailureBody()` / `createLegacyIdentityAuthFailureBody()` / `createAuthFailureBody(scope, code, message)` / `AuthFailureScope` | Stable wire contract for distinguishing a lost global identity (`identity`) from recent-login (`reauthentication`) and feature credential (`credential`) failures. The legacy helper tags products whose installed clients still require auth failure as `403`. |
82
83
  | `perfLog(options?)` / `PerfLogOptions` / `AnalyticsEngineDatasetLike` | Middleware that records one per-request latency data point (`t_app`, colo, cold/warm, route, status) and emits it to **Workers Logs** (`console.log`) and/or **Workers Analytics Engine** (`writeDataPoint`). Lets you measure low-traffic Workers without a live `wrangler tail`. |
83
84
  | `createMaintenanceMiddleware(options)` / `createMaintenanceWaitHandler(options)` / `isMaintenanceEnabled(env)` / `MAINTENANCE_CODE` / `MAINTENANCE_WAIT_PATH` | Fleet maintenance short-circuit: when enabled (`MAINTENANCE=1`), every non-allowlisted request returns `503` + `{ statusCode, message, code: 'MAINTENANCE' }` **before** container/DB. Pair with `GET /public/maintenance/wait` SSE (`event: ping` / `event: ended`) so clients can auto-dismiss a lock UI. Mount after `cors`, before `containerMiddleware`. |
84
85
  | `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createHttpErrorHandler`'s `onUnhandledError`. |
@@ -461,7 +462,7 @@ verify/resolver, context-variable names, and failure mode.
461
462
  `setContext` is type-checked against your `Variables`.
462
463
 
463
464
  ```ts
464
- import { createAuthMiddleware } from '@rdlabo/workers-hono-kit';
465
+ import { createAuthMiddleware, createIdentityAuthFailureBody } from '@rdlabo/workers-hono-kit';
465
466
 
466
467
  // AuthGuard: verify + resolve (and provision) the DB user id.
467
468
  const userAuth = createAuthMiddleware<AppEnv, UserRecord, number>({
@@ -473,16 +474,28 @@ const userAuth = createAuthMiddleware<AppEnv, UserRecord, number>({
473
474
  c.set('userId', userId);
474
475
  c.set('appInfo', appInfo);
475
476
  },
477
+ onFailure: (_error, context) =>
478
+ context.json(createIdentityAuthFailureBody(), 401),
476
479
  });
477
480
 
478
481
  // TokenGuard (login): verify only — omit resolveUserId. Override the failure if needed.
479
482
  const tokenAuth = createAuthMiddleware<AppEnv, UserRecord>({
480
483
  verify: (token) => container.firebase.verifyIdToken(token),
481
484
  setContext: (c, { verified }) => c.set('userRecord', verified),
482
- onFailure: (_e, c) => c.json({ message: 'Unauthorized', statusCode: 401 }, 401),
485
+ onFailure: (_e, c) => c.json(createIdentityAuthFailureBody(), 401),
483
486
  });
484
487
  ```
485
488
 
489
+ Authentication failures use three explicit scopes. Only `identity` permits a client to purge its
490
+ global authenticated session, offline replica boundary, and outbox. `reauthentication` means the
491
+ identity remains valid but a recent sign-in is required; `credential` belongs to a domain feature
492
+ such as a public booking token. New APIs use `401`; products with installed clients that historically
493
+ interpret auth failure as `403` use `createLegacyIdentityAuthFailureBody()` until that compatibility
494
+ contract can be retired. An untagged `403` is an authenticated permission/business denial and must
495
+ not be used as a global-session invalidation signal. Domain-specific `code` values remain product-owned.
496
+ `createAuthMiddleware` retains its historical untagged `403` default for source/runtime compatibility;
497
+ the tagged identity contract is an explicit `onFailure` opt-in as shown above.
498
+
486
499
  ### Latency instrumentation (`perfLog`)
487
500
 
488
501
  Records one data point per request — `t_app` (time inside the app), `colo`, `cold`/`warm`, matched
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Describes which credential boundary caused an authentication failure.
3
+ *
4
+ * Only `identity` means that the application's global authenticated identity is
5
+ * no longer usable. `reauthentication` keeps the identity but requires a recent
6
+ * sign-in, while `credential` is limited to a delegated/domain credential.
7
+ */
8
+ export declare const AUTH_FAILURE_SCOPES: {
9
+ readonly identity: "identity";
10
+ readonly reauthentication: "reauthentication";
11
+ readonly credential: "credential";
12
+ };
13
+ export type AuthFailureScope = (typeof AUTH_FAILURE_SCOPES)[keyof typeof AUTH_FAILURE_SCOPES];
14
+ export declare const AUTH_IDENTITY_INVALID_CODE = "AUTH_IDENTITY_INVALID";
15
+ export interface AuthFailureBody<TScope extends AuthFailureScope = AuthFailureScope, TCode extends string = string, TStatus extends 401 | 403 = 401> {
16
+ statusCode: TStatus;
17
+ message: string;
18
+ code: TCode;
19
+ authFailureScope: TScope;
20
+ }
21
+ /**
22
+ * Creates the stable wire body for an authentication failure.
23
+ *
24
+ * Domain-specific codes remain owned by each application. The scope is the
25
+ * shared lifecycle contract consumed by clients deciding which local state may
26
+ * be invalidated.
27
+ */
28
+ export declare const createAuthFailureBody: <TScope extends AuthFailureScope, TCode extends string>(authFailureScope: TScope, code: TCode, message: string) => AuthFailureBody<TScope, TCode>;
29
+ export declare const createIdentityAuthFailureBody: (message?: string) => AuthFailureBody<"identity", typeof AUTH_IDENTITY_INVALID_CODE>;
30
+ /**
31
+ * Creates an explicitly tagged identity failure with the historical HTTP 403
32
+ * status used by some deployed products.
33
+ *
34
+ * New APIs should use {@link createIdentityAuthFailureBody}. This helper exists
35
+ * for servers that must remain compatible with installed clients whose auth
36
+ * interceptor recognizes the legacy 403 response.
37
+ */
38
+ export declare const createLegacyIdentityAuthFailureBody: (message?: string) => AuthFailureBody<"identity", typeof AUTH_IDENTITY_INVALID_CODE, 403>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Describes which credential boundary caused an authentication failure.
3
+ *
4
+ * Only `identity` means that the application's global authenticated identity is
5
+ * no longer usable. `reauthentication` keeps the identity but requires a recent
6
+ * sign-in, while `credential` is limited to a delegated/domain credential.
7
+ */
8
+ export const AUTH_FAILURE_SCOPES = {
9
+ identity: 'identity',
10
+ reauthentication: 'reauthentication',
11
+ credential: 'credential',
12
+ };
13
+ export const AUTH_IDENTITY_INVALID_CODE = 'AUTH_IDENTITY_INVALID';
14
+ /**
15
+ * Creates the stable wire body for an authentication failure.
16
+ *
17
+ * Domain-specific codes remain owned by each application. The scope is the
18
+ * shared lifecycle contract consumed by clients deciding which local state may
19
+ * be invalidated.
20
+ */
21
+ export const createAuthFailureBody = (authFailureScope, code, message) => ({
22
+ statusCode: 401,
23
+ message,
24
+ code,
25
+ authFailureScope,
26
+ });
27
+ export const createIdentityAuthFailureBody = (message = 'Unauthorized') => createAuthFailureBody(AUTH_FAILURE_SCOPES.identity, AUTH_IDENTITY_INVALID_CODE, message);
28
+ /**
29
+ * Creates an explicitly tagged identity failure with the historical HTTP 403
30
+ * status used by some deployed products.
31
+ *
32
+ * New APIs should use {@link createIdentityAuthFailureBody}. This helper exists
33
+ * for servers that must remain compatible with installed clients whose auth
34
+ * interceptor recognizes the legacy 403 response.
35
+ */
36
+ export const createLegacyIdentityAuthFailureBody = (message = 'Forbidden resource') => ({
37
+ statusCode: 403,
38
+ message,
39
+ code: AUTH_IDENTITY_INVALID_CODE,
40
+ authFailureScope: AUTH_FAILURE_SCOPES.identity,
41
+ });
package/dist/index.d.ts CHANGED
@@ -47,6 +47,8 @@ export { defaultDefer, createWaitUntilDefer } from './http/defer.js';
47
47
  export type { DeferExecutor } from './http/defer.js';
48
48
  export { createSentryErrorReporter } from './http/http-error.js';
49
49
  export type { SentryExceptionReporterLike } from './http/http-error.js';
50
+ export { AUTH_FAILURE_SCOPES, AUTH_IDENTITY_INVALID_CODE, createAuthFailureBody, createIdentityAuthFailureBody, createLegacyIdentityAuthFailureBody, } from './http/auth-failure.js';
51
+ export type { AuthFailureBody, AuthFailureScope } from './http/auth-failure.js';
50
52
  export { canonicalJson, createIdempotencyInput, IdempotencyConflictError, IdempotencyInFlightError, IdempotencyKeyValidationError, IdempotencyPayloadValidationError, runIdempotentMutation, sha256CanonicalJson, withIdempotencyHttpErrors, } from './idempotency/idempotency.js';
51
53
  export type { CreateIdempotencyInputOptions, IdempotencyInput, IdempotencyReservation, IdempotencyScope, IdempotencyScopeValue, IdempotentMutationStore, } from './idempotency/idempotency.js';
52
54
  export { acknowledgeHibernationWebSocketClose, broadcastHibernationWebSockets, closeHibernationWebSocket, configureHibernationAutoResponse, upgradeHibernationWebSocket, } from './realtime/hibernation.js';
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ export { createAppErrorHandler } from './http/app-error-handler.js';
35
35
  export { normalizeTrailingSlash } from './http/trailing-slash.js';
36
36
  export { defaultDefer, createWaitUntilDefer } from './http/defer.js';
37
37
  export { createSentryErrorReporter } from './http/http-error.js';
38
+ export { AUTH_FAILURE_SCOPES, AUTH_IDENTITY_INVALID_CODE, createAuthFailureBody, createIdentityAuthFailureBody, createLegacyIdentityAuthFailureBody, } from './http/auth-failure.js';
38
39
  // idempotency
39
40
  export { canonicalJson, createIdempotencyInput, IdempotencyConflictError, IdempotencyInFlightError, IdempotencyKeyValidationError, IdempotencyPayloadValidationError, runIdempotentMutation, sha256CanonicalJson, withIdempotencyHttpErrors, } from './idempotency/idempotency.js';
40
41
  // realtime
@@ -10,3 +10,5 @@ export { fromTinyIntFlag, replicaTimestampMs, toReplicaDateOnly, toReplicaIsoDat
10
10
  export { replicaNowIso } from './clock.js';
11
11
  export { defineRestDbMethodConverter } from './rest-db-method-converter.js';
12
12
  export type { RestDbMethodConverter } from './rest-db-method-converter.js';
13
+ export { decodeOfflineSnapshotCursor, encodeOfflineSnapshotCursor } from './snapshot-cursor.js';
14
+ export type { OfflineSnapshotCursor } from './snapshot-cursor.js';
@@ -9,3 +9,4 @@
9
9
  export { fromTinyIntFlag, replicaTimestampMs, toReplicaDateOnly, toReplicaIsoDatetime, toTinyIntFlag } from './wire.js';
10
10
  export { replicaNowIso } from './clock.js';
11
11
  export { defineRestDbMethodConverter } from './rest-db-method-converter.js';
12
+ export { decodeOfflineSnapshotCursor, encodeOfflineSnapshotCursor } from './snapshot-cursor.js';
@@ -0,0 +1,13 @@
1
+ /** Position of a keyset-paginated initial snapshot at a captured journal watermark. */
2
+ export interface OfflineSnapshotCursor {
3
+ /** Highest journal revision captured before the snapshot scan starts. */
4
+ watermark: number;
5
+ /** Zero-based product-defined snapshot source index. */
6
+ sourceIndex: number;
7
+ /** Last server-side numeric ID emitted from the current source. */
8
+ afterId: number;
9
+ }
10
+ /** Encodes an initial-snapshot position as a stable versioned cursor. */
11
+ export declare function encodeOfflineSnapshotCursor(cursor: OfflineSnapshotCursor): string;
12
+ /** Decodes a versioned initial-snapshot cursor, returning null for malformed input. */
13
+ export declare function decodeOfflineSnapshotCursor(value: string): OfflineSnapshotCursor | null;
@@ -0,0 +1,39 @@
1
+ const SNAPSHOT_CURSOR_PREFIX = 'snapshot:v1';
2
+ const CANONICAL_NON_NEGATIVE_INTEGER = /^(0|[1-9]\d*)$/;
3
+ /** Encodes an initial-snapshot position as a stable versioned cursor. */
4
+ export function encodeOfflineSnapshotCursor(cursor) {
5
+ assertNonNegativeSafeInteger(cursor.watermark, 'watermark');
6
+ assertNonNegativeSafeInteger(cursor.sourceIndex, 'sourceIndex');
7
+ assertNonNegativeSafeInteger(cursor.afterId, 'afterId');
8
+ return `${SNAPSHOT_CURSOR_PREFIX}:${cursor.watermark}:${cursor.sourceIndex}:${cursor.afterId}`;
9
+ }
10
+ /** Decodes a versioned initial-snapshot cursor, returning null for malformed input. */
11
+ export function decodeOfflineSnapshotCursor(value) {
12
+ const parts = value.split(':');
13
+ if (parts.length !== 5 || `${parts[0]}:${parts[1]}` !== SNAPSHOT_CURSOR_PREFIX) {
14
+ return null;
15
+ }
16
+ const [, , watermarkValue, sourceIndexValue, afterIdValue] = parts;
17
+ if (!CANONICAL_NON_NEGATIVE_INTEGER.test(watermarkValue) ||
18
+ !CANONICAL_NON_NEGATIVE_INTEGER.test(sourceIndexValue) ||
19
+ !CANONICAL_NON_NEGATIVE_INTEGER.test(afterIdValue)) {
20
+ return null;
21
+ }
22
+ const watermark = Number(watermarkValue);
23
+ const sourceIndex = Number(sourceIndexValue);
24
+ const afterId = Number(afterIdValue);
25
+ if (!isNonNegativeSafeInteger(watermark) ||
26
+ !isNonNegativeSafeInteger(sourceIndex) ||
27
+ !isNonNegativeSafeInteger(afterId)) {
28
+ return null;
29
+ }
30
+ return { watermark, sourceIndex, afterId };
31
+ }
32
+ function isNonNegativeSafeInteger(value) {
33
+ return Number.isSafeInteger(value) && value >= 0;
34
+ }
35
+ function assertNonNegativeSafeInteger(value, field) {
36
+ if (!isNonNegativeSafeInteger(value)) {
37
+ throw new RangeError(`Offline snapshot cursor ${field} must be a non-negative safe integer.`);
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/workers-hono-kit",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"