@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 +15 -2
- package/dist/http/auth-failure.d.ts +38 -0
- package/dist/http/auth-failure.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/offline/index.d.ts +2 -0
- package/dist/offline/index.js +1 -0
- package/dist/offline/snapshot-cursor.d.ts +13 -0
- package/dist/offline/snapshot-cursor.js +39 -0
- package/package.json +1 -1
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(
|
|
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
|
package/dist/offline/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/offline/index.js
CHANGED
|
@@ -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
|
+
}
|