@de-otio/trellis 0.6.0 → 0.7.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/dist/env.d.ts +21 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +12 -0
- package/dist/env.js.map +1 -1
- package/dist/lambda/nightly-cron.d.ts.map +1 -1
- package/dist/lambda/nightly-cron.js +5 -2
- package/dist/lambda/nightly-cron.js.map +1 -1
- package/dist/lambda/post-confirmation.d.ts +30 -0
- package/dist/lambda/post-confirmation.d.ts.map +1 -1
- package/dist/lambda/post-confirmation.js +333 -29
- package/dist/lambda/post-confirmation.js.map +1 -1
- package/dist/lambda/pre-token-generation.d.ts +20 -0
- package/dist/lambda/pre-token-generation.d.ts.map +1 -1
- package/dist/lambda/pre-token-generation.js +233 -48
- package/dist/lambda/pre-token-generation.js.map +1 -1
- package/dist/lib/activitypub/activity-processor.d.ts.map +1 -1
- package/dist/lib/activitypub/activity-processor.js +2 -1
- package/dist/lib/activitypub/activity-processor.js.map +1 -1
- package/dist/lib/activitypub/group-service.d.ts +2 -2
- package/dist/lib/activitypub/group-service.d.ts.map +1 -1
- package/dist/lib/activitypub/group-service.js +5 -2
- package/dist/lib/activitypub/group-service.js.map +1 -1
- package/dist/lib/age-tier-transition.d.ts.map +1 -1
- package/dist/lib/age-tier-transition.js +19 -10
- package/dist/lib/age-tier-transition.js.map +1 -1
- package/dist/lib/audit/csv-export.d.ts +25 -0
- package/dist/lib/audit/csv-export.d.ts.map +1 -0
- package/dist/lib/audit/csv-export.js +54 -0
- package/dist/lib/audit/csv-export.js.map +1 -0
- package/dist/lib/audit/emit.d.ts +56 -0
- package/dist/lib/audit/emit.d.ts.map +1 -0
- package/dist/lib/audit/emit.js +124 -0
- package/dist/lib/audit/emit.js.map +1 -0
- package/dist/lib/audit/event-types.d.ts +36 -0
- package/dist/lib/audit/event-types.d.ts.map +1 -0
- package/dist/lib/audit/event-types.js +69 -0
- package/dist/lib/audit/event-types.js.map +1 -0
- package/dist/lib/audit/pii-filter.d.ts +22 -0
- package/dist/lib/audit/pii-filter.d.ts.map +1 -0
- package/dist/lib/audit/pii-filter.js +51 -0
- package/dist/lib/audit/pii-filter.js.map +1 -0
- package/dist/lib/audit-logger.js +1 -1
- package/dist/lib/audit-logger.js.map +1 -1
- package/dist/lib/auth/auth-context.d.ts +34 -0
- package/dist/lib/auth/auth-context.d.ts.map +1 -0
- package/dist/lib/auth/auth-context.js +10 -0
- package/dist/lib/auth/auth-context.js.map +1 -0
- package/dist/lib/auth/auth-middleware.d.ts +50 -0
- package/dist/lib/auth/auth-middleware.d.ts.map +1 -0
- package/dist/lib/auth/auth-middleware.js +153 -0
- package/dist/lib/auth/auth-middleware.js.map +1 -0
- package/dist/lib/auth/capabilities.d.ts +40 -0
- package/dist/lib/auth/capabilities.d.ts.map +1 -0
- package/dist/lib/auth/capabilities.js +44 -0
- package/dist/lib/auth/capabilities.js.map +1 -0
- package/dist/lib/auth/claims-cache.d.ts +70 -0
- package/dist/lib/auth/claims-cache.d.ts.map +1 -0
- package/dist/lib/auth/claims-cache.js +139 -0
- package/dist/lib/auth/claims-cache.js.map +1 -0
- package/dist/lib/auth/cognito-jwt.d.ts +6 -0
- package/dist/lib/auth/cognito-jwt.d.ts.map +1 -1
- package/dist/lib/auth/cognito-jwt.js.map +1 -1
- package/dist/lib/auth/idp-redirect-builder.d.ts +43 -0
- package/dist/lib/auth/idp-redirect-builder.d.ts.map +1 -0
- package/dist/lib/auth/idp-redirect-builder.js +48 -0
- package/dist/lib/auth/idp-redirect-builder.js.map +1 -0
- package/dist/lib/auth/require.d.ts +51 -0
- package/dist/lib/auth/require.d.ts.map +1 -0
- package/dist/lib/auth/require.js +99 -0
- package/dist/lib/auth/require.js.map +1 -0
- package/dist/lib/auth/role-grants.d.ts +18 -0
- package/dist/lib/auth/role-grants.d.ts.map +1 -0
- package/dist/lib/auth/role-grants.js +62 -0
- package/dist/lib/auth/role-grants.js.map +1 -0
- package/dist/lib/cognito/idp-sdk.d.ts +80 -0
- package/dist/lib/cognito/idp-sdk.d.ts.map +1 -0
- package/dist/lib/cognito/idp-sdk.js +186 -0
- package/dist/lib/cognito/idp-sdk.js.map +1 -0
- package/dist/lib/cognito/issuer-probe.d.ts +47 -0
- package/dist/lib/cognito/issuer-probe.d.ts.map +1 -0
- package/dist/lib/cognito/issuer-probe.js +319 -0
- package/dist/lib/cognito/issuer-probe.js.map +1 -0
- package/dist/lib/comment-handler.d.ts +7 -7
- package/dist/lib/comment-handler.d.ts.map +1 -1
- package/dist/lib/comment-handler.js +23 -20
- package/dist/lib/comment-handler.js.map +1 -1
- package/dist/lib/compliance/baseline.d.ts +15 -0
- package/dist/lib/compliance/baseline.d.ts.map +1 -0
- package/dist/lib/compliance/baseline.js +205 -0
- package/dist/lib/compliance/baseline.js.map +1 -0
- package/dist/lib/compliance/tenant-merge.d.ts +35 -0
- package/dist/lib/compliance/tenant-merge.d.ts.map +1 -0
- package/dist/lib/compliance/tenant-merge.js +80 -0
- package/dist/lib/compliance/tenant-merge.js.map +1 -0
- package/dist/lib/compliance/types.d.ts +135 -0
- package/dist/lib/compliance/types.d.ts.map +1 -0
- package/dist/lib/compliance/types.js +9 -0
- package/dist/lib/compliance/types.js.map +1 -0
- package/dist/lib/connection-code-handler.d.ts +4 -4
- package/dist/lib/connection-code-handler.d.ts.map +1 -1
- package/dist/lib/connection-code-handler.js +21 -11
- package/dist/lib/connection-code-handler.js.map +1 -1
- package/dist/lib/feed-handler.d.ts +2 -2
- package/dist/lib/feed-handler.d.ts.map +1 -1
- package/dist/lib/feed-handler.js +5 -9
- package/dist/lib/feed-handler.js.map +1 -1
- package/dist/lib/middleware/idempotency-store.d.ts +86 -0
- package/dist/lib/middleware/idempotency-store.d.ts.map +1 -0
- package/dist/lib/middleware/idempotency-store.js +109 -0
- package/dist/lib/middleware/idempotency-store.js.map +1 -0
- package/dist/lib/middleware/idempotency.d.ts +37 -0
- package/dist/lib/middleware/idempotency.d.ts.map +1 -0
- package/dist/lib/middleware/idempotency.js +358 -0
- package/dist/lib/middleware/idempotency.js.map +1 -0
- package/dist/lib/net/trusted-client-ip.d.ts +39 -0
- package/dist/lib/net/trusted-client-ip.d.ts.map +1 -0
- package/dist/lib/net/trusted-client-ip.js +100 -0
- package/dist/lib/net/trusted-client-ip.js.map +1 -0
- package/dist/lib/notification-handler.d.ts +5 -5
- package/dist/lib/notification-handler.d.ts.map +1 -1
- package/dist/lib/notification-handler.js +11 -9
- package/dist/lib/notification-handler.js.map +1 -1
- package/dist/lib/oauth/cognito-issuer.d.ts +34 -0
- package/dist/lib/oauth/cognito-issuer.d.ts.map +1 -0
- package/dist/lib/oauth/cognito-issuer.js +53 -0
- package/dist/lib/oauth/cognito-issuer.js.map +1 -0
- package/dist/lib/oauth/device-authorization.d.ts +145 -0
- package/dist/lib/oauth/device-authorization.d.ts.map +1 -0
- package/dist/lib/oauth/device-authorization.js +312 -0
- package/dist/lib/oauth/device-authorization.js.map +1 -0
- package/dist/lib/oauth/envelope-crypto.d.ts +101 -0
- package/dist/lib/oauth/envelope-crypto.d.ts.map +1 -0
- package/dist/lib/oauth/envelope-crypto.js +223 -0
- package/dist/lib/oauth/envelope-crypto.js.map +1 -0
- package/dist/lib/oauth/refresh-detection.d.ts +126 -0
- package/dist/lib/oauth/refresh-detection.d.ts.map +1 -0
- package/dist/lib/oauth/refresh-detection.js +248 -0
- package/dist/lib/oauth/refresh-detection.js.map +1 -0
- package/dist/lib/openapi/generator.d.ts +78 -0
- package/dist/lib/openapi/generator.d.ts.map +1 -0
- package/dist/lib/openapi/generator.js +201 -0
- package/dist/lib/openapi/generator.js.map +1 -0
- package/dist/lib/post-handler.d.ts +1 -1
- package/dist/lib/post-handler.d.ts.map +1 -1
- package/dist/lib/post-handler.js +4 -15
- package/dist/lib/post-handler.js.map +1 -1
- package/dist/lib/rate-limit.d.ts.map +1 -1
- package/dist/lib/rate-limit.js +11 -3
- package/dist/lib/rate-limit.js.map +1 -1
- package/dist/lib/routes/agent-authorize.d.ts +32 -0
- package/dist/lib/routes/agent-authorize.d.ts.map +1 -0
- package/dist/lib/routes/agent-authorize.js +479 -0
- package/dist/lib/routes/agent-authorize.js.map +1 -0
- package/dist/lib/routes/agent-sessions.d.ts +20 -0
- package/dist/lib/routes/agent-sessions.d.ts.map +1 -0
- package/dist/lib/routes/agent-sessions.js +124 -0
- package/dist/lib/routes/agent-sessions.js.map +1 -0
- package/dist/lib/routes/agent-surface.d.ts +37 -0
- package/dist/lib/routes/agent-surface.d.ts.map +1 -0
- package/dist/lib/routes/agent-surface.js +208 -0
- package/dist/lib/routes/agent-surface.js.map +1 -0
- package/dist/lib/routes/auth-discover.d.ts +18 -0
- package/dist/lib/routes/auth-discover.d.ts.map +1 -0
- package/dist/lib/routes/auth-discover.js +177 -0
- package/dist/lib/routes/auth-discover.js.map +1 -0
- package/dist/lib/routes/comments.d.ts.map +1 -1
- package/dist/lib/routes/comments.js +36 -7
- package/dist/lib/routes/comments.js.map +1 -1
- package/dist/lib/routes/connection-codes.d.ts.map +1 -1
- package/dist/lib/routes/connection-codes.js +21 -4
- package/dist/lib/routes/connection-codes.js.map +1 -1
- package/dist/lib/routes/content-discovery.d.ts.map +1 -1
- package/dist/lib/routes/content-discovery.js +18 -13
- package/dist/lib/routes/content-discovery.js.map +1 -1
- package/dist/lib/routes/dashboard.js +1 -1
- package/dist/lib/routes/dashboard.js.map +1 -1
- package/dist/lib/routes/employees.d.ts.map +1 -1
- package/dist/lib/routes/employees.js +57 -15
- package/dist/lib/routes/employees.js.map +1 -1
- package/dist/lib/routes/entities.d.ts.map +1 -1
- package/dist/lib/routes/entities.js +35 -19
- package/dist/lib/routes/entities.js.map +1 -1
- package/dist/lib/routes/errors.d.ts +34 -0
- package/dist/lib/routes/errors.d.ts.map +1 -0
- package/dist/lib/routes/errors.js +57 -0
- package/dist/lib/routes/errors.js.map +1 -0
- package/dist/lib/routes/feeds.d.ts.map +1 -1
- package/dist/lib/routes/feeds.js +12 -2
- package/dist/lib/routes/feeds.js.map +1 -1
- package/dist/lib/routes/index.d.ts.map +1 -1
- package/dist/lib/routes/index.js +50 -0
- package/dist/lib/routes/index.js.map +1 -1
- package/dist/lib/routes/mfa.d.ts.map +1 -1
- package/dist/lib/routes/mfa.js +1 -0
- package/dist/lib/routes/mfa.js.map +1 -1
- package/dist/lib/routes/notifications.d.ts.map +1 -1
- package/dist/lib/routes/notifications.js +21 -4
- package/dist/lib/routes/notifications.js.map +1 -1
- package/dist/lib/routes/oauth.d.ts +15 -0
- package/dist/lib/routes/oauth.d.ts.map +1 -0
- package/dist/lib/routes/oauth.js +139 -0
- package/dist/lib/routes/oauth.js.map +1 -0
- package/dist/lib/routes/posts.d.ts.map +1 -1
- package/dist/lib/routes/posts.js +30 -19
- package/dist/lib/routes/posts.js.map +1 -1
- package/dist/lib/routes/products.d.ts.map +1 -1
- package/dist/lib/routes/products.js +19 -22
- package/dist/lib/routes/products.js.map +1 -1
- package/dist/lib/routes/setup-status.d.ts +34 -0
- package/dist/lib/routes/setup-status.d.ts.map +1 -0
- package/dist/lib/routes/setup-status.js +87 -0
- package/dist/lib/routes/setup-status.js.map +1 -0
- package/dist/lib/routes/taxonomy-analytics.d.ts.map +1 -1
- package/dist/lib/routes/taxonomy-analytics.js +15 -14
- package/dist/lib/routes/taxonomy-analytics.js.map +1 -1
- package/dist/lib/routes/taxonomy.d.ts.map +1 -1
- package/dist/lib/routes/taxonomy.js +19 -16
- package/dist/lib/routes/taxonomy.js.map +1 -1
- package/dist/lib/routes/tenant-audit.d.ts +19 -0
- package/dist/lib/routes/tenant-audit.d.ts.map +1 -0
- package/dist/lib/routes/tenant-audit.js +244 -0
- package/dist/lib/routes/tenant-audit.js.map +1 -0
- package/dist/lib/routes/tenant-compliance.d.ts +21 -0
- package/dist/lib/routes/tenant-compliance.d.ts.map +1 -0
- package/dist/lib/routes/tenant-compliance.js +122 -0
- package/dist/lib/routes/tenant-compliance.js.map +1 -0
- package/dist/lib/routes/tenant-domains.d.ts +11 -0
- package/dist/lib/routes/tenant-domains.d.ts.map +1 -0
- package/dist/lib/routes/tenant-domains.js +95 -0
- package/dist/lib/routes/tenant-domains.js.map +1 -0
- package/dist/lib/routes/tenant-idp.d.ts +3 -0
- package/dist/lib/routes/tenant-idp.d.ts.map +1 -0
- package/dist/lib/routes/tenant-idp.js +89 -0
- package/dist/lib/routes/tenant-idp.js.map +1 -0
- package/dist/lib/routes/tenant-members.d.ts +13 -0
- package/dist/lib/routes/tenant-members.d.ts.map +1 -0
- package/dist/lib/routes/tenant-members.js +75 -0
- package/dist/lib/routes/tenant-members.js.map +1 -0
- package/dist/lib/routes/tenant-role-mappings.d.ts +11 -0
- package/dist/lib/routes/tenant-role-mappings.d.ts.map +1 -0
- package/dist/lib/routes/tenant-role-mappings.js +90 -0
- package/dist/lib/routes/tenant-role-mappings.js.map +1 -0
- package/dist/lib/routes/tenants.d.ts +13 -0
- package/dist/lib/routes/tenants.d.ts.map +1 -0
- package/dist/lib/routes/tenants.js +121 -0
- package/dist/lib/routes/tenants.js.map +1 -0
- package/dist/lib/routes/types.d.ts +9 -0
- package/dist/lib/routes/types.d.ts.map +1 -1
- package/dist/lib/schemas.d.ts +2 -2
- package/dist/lib/secrets/idp-secrets.d.ts +51 -0
- package/dist/lib/secrets/idp-secrets.d.ts.map +1 -0
- package/dist/lib/secrets/idp-secrets.js +111 -0
- package/dist/lib/secrets/idp-secrets.js.map +1 -0
- package/dist/lib/security-monitor.d.ts.map +1 -1
- package/dist/lib/security-monitor.js +6 -1
- package/dist/lib/security-monitor.js.map +1 -1
- package/dist/lib/session-manager.d.ts +1 -0
- package/dist/lib/session-manager.d.ts.map +1 -1
- package/dist/lib/session-manager.js.map +1 -1
- package/dist/lib/taxonomy-handler-factory.d.ts +4 -2
- package/dist/lib/taxonomy-handler-factory.d.ts.map +1 -1
- package/dist/lib/taxonomy-handler-factory.js +8 -7
- package/dist/lib/taxonomy-handler-factory.js.map +1 -1
- package/dist/lib/tenant/audit-emit.d.ts +18 -0
- package/dist/lib/tenant/audit-emit.d.ts.map +1 -0
- package/dist/lib/tenant/audit-emit.js +16 -0
- package/dist/lib/tenant/audit-emit.js.map +1 -0
- package/dist/lib/tenant/derive-domain.d.ts +19 -0
- package/dist/lib/tenant/derive-domain.d.ts.map +1 -0
- package/dist/lib/tenant/derive-domain.js +38 -0
- package/dist/lib/tenant/derive-domain.js.map +1 -0
- package/dist/lib/tenant/domain-handler.d.ts +42 -0
- package/dist/lib/tenant/domain-handler.d.ts.map +1 -0
- package/dist/lib/tenant/domain-handler.js +344 -0
- package/dist/lib/tenant/domain-handler.js.map +1 -0
- package/dist/lib/tenant/domain-validator.d.ts +28 -0
- package/dist/lib/tenant/domain-validator.d.ts.map +1 -0
- package/dist/lib/tenant/domain-validator.js +145 -0
- package/dist/lib/tenant/domain-validator.js.map +1 -0
- package/dist/lib/tenant/domain-verifier.d.ts +30 -0
- package/dist/lib/tenant/domain-verifier.d.ts.map +1 -0
- package/dist/lib/tenant/domain-verifier.js +53 -0
- package/dist/lib/tenant/domain-verifier.js.map +1 -0
- package/dist/lib/tenant/idp-handler.d.ts +29 -0
- package/dist/lib/tenant/idp-handler.d.ts.map +1 -0
- package/dist/lib/tenant/idp-handler.js +693 -0
- package/dist/lib/tenant/idp-handler.js.map +1 -0
- package/dist/lib/tenant/idp-name.d.ts +2 -0
- package/dist/lib/tenant/idp-name.d.ts.map +1 -0
- package/dist/lib/tenant/idp-name.js +20 -0
- package/dist/lib/tenant/idp-name.js.map +1 -0
- package/dist/lib/tenant/member-handler.d.ts +31 -0
- package/dist/lib/tenant/member-handler.d.ts.map +1 -0
- package/dist/lib/tenant/member-handler.js +343 -0
- package/dist/lib/tenant/member-handler.js.map +1 -0
- package/dist/lib/tenant/reserved-slugs.d.ts +37 -0
- package/dist/lib/tenant/reserved-slugs.d.ts.map +1 -0
- package/dist/lib/tenant/reserved-slugs.js +116 -0
- package/dist/lib/tenant/reserved-slugs.js.map +1 -0
- package/dist/lib/tenant/resolve-role.d.ts +39 -0
- package/dist/lib/tenant/resolve-role.d.ts.map +1 -0
- package/dist/lib/tenant/resolve-role.js +60 -0
- package/dist/lib/tenant/resolve-role.js.map +1 -0
- package/dist/lib/tenant/role-mapping-handler.d.ts +26 -0
- package/dist/lib/tenant/role-mapping-handler.d.ts.map +1 -0
- package/dist/lib/tenant/role-mapping-handler.js +260 -0
- package/dist/lib/tenant/role-mapping-handler.js.map +1 -0
- package/dist/lib/tenant/setup-status.d.ts +83 -0
- package/dist/lib/tenant/setup-status.d.ts.map +1 -0
- package/dist/lib/tenant/setup-status.js +201 -0
- package/dist/lib/tenant/setup-status.js.map +1 -0
- package/dist/lib/tenant/slug-validator.d.ts +31 -0
- package/dist/lib/tenant/slug-validator.d.ts.map +1 -0
- package/dist/lib/tenant/slug-validator.js +42 -0
- package/dist/lib/tenant/slug-validator.js.map +1 -0
- package/dist/lib/tenant/tenant-handler.d.ts +49 -0
- package/dist/lib/tenant/tenant-handler.d.ts.map +1 -0
- package/dist/lib/tenant/tenant-handler.js +377 -0
- package/dist/lib/tenant/tenant-handler.js.map +1 -0
- package/dist/lib/tenant/transfer-ownership.d.ts +39 -0
- package/dist/lib/tenant/transfer-ownership.d.ts.map +1 -0
- package/dist/lib/tenant/transfer-ownership.js +66 -0
- package/dist/lib/tenant/transfer-ownership.js.map +1 -0
- package/dist/lib/user/derive-handle.d.ts +29 -0
- package/dist/lib/user/derive-handle.d.ts.map +1 -0
- package/dist/lib/user/derive-handle.js +65 -0
- package/dist/lib/user/derive-handle.js.map +1 -0
- package/dist/lib/user-deprovisioning.d.ts +11 -1
- package/dist/lib/user-deprovisioning.d.ts.map +1 -1
- package/dist/lib/user-deprovisioning.js +46 -2
- package/dist/lib/user-deprovisioning.js.map +1 -1
- package/dist/lib/validation/feature-toggle-schemas.d.ts +10 -10
- package/package.json +6 -3
- package/prisma/migrations/20260502094501_add_tenancy_model/migration.sql +334 -0
- package/prisma/migrations/20260503000000_add_tenant_region/migration.sql +4 -0
- package/prisma/schema.prisma +324 -74
- package/src/lambda/nightly-cron.ts +4 -1
- package/src/lambda/post-confirmation.ts +405 -29
- package/src/lambda/pre-token-generation.ts +300 -59
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../../src/lib/middleware/idempotency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAOL,KAAK,yBAAyB,EAG/B,MAAM,qBAAqB,CAAC;AA0F7B;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAKvF;AAMD,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAMD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,CAAC,EAAE,yBAAyB,GACxC,UAAU,CAiJZ"}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Idempotency-Key Middleware (T9b-c)
|
|
4
|
+
*
|
|
5
|
+
* Deduplicates POST requests using an `Idempotency-Key` header backed by a
|
|
6
|
+
* 24-hour DynamoDB cache.
|
|
7
|
+
*
|
|
8
|
+
* Behaviour:
|
|
9
|
+
* - No header → pass-through (no caching, handler runs normally).
|
|
10
|
+
* - First request with key K + body B → execute handler, cache result,
|
|
11
|
+
* respond with `Idempotency-Replay: false`.
|
|
12
|
+
* - Repeat with same key K + same body B → return cached response,
|
|
13
|
+
* `Idempotency-Replay: true`. Handler NOT called.
|
|
14
|
+
* - Repeat with same key K + different body B' → 422 IDEMPOTENCY_KEY_REUSE.
|
|
15
|
+
* - Non-POST method with key present → pass-through (key is only honoured on POST).
|
|
16
|
+
* - Body > 1 MB → 413 IDEMPOTENCY_BODY_TOO_LARGE.
|
|
17
|
+
* - Key invalid (empty / non-ASCII-printable / > 255 chars) → 400 IDEMPOTENCY_KEY_INVALID.
|
|
18
|
+
* - Concurrent same-key claim (race): loser polls up to 3×100 ms for winner's
|
|
19
|
+
* response, then returns 409 IDEMPOTENCY_KEY_IN_FLIGHT.
|
|
20
|
+
*
|
|
21
|
+
* Spec: plans/mvp/10-trellis-stages/09-agent-surface.md §T9b-c
|
|
22
|
+
* doc/02-technical/identity-federation/10-agent-friendly-onboarding.md §MVP-4
|
|
23
|
+
*/
|
|
24
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
27
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
28
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
29
|
+
}
|
|
30
|
+
Object.defineProperty(o, k2, desc);
|
|
31
|
+
}) : (function(o, m, k, k2) {
|
|
32
|
+
if (k2 === undefined) k2 = k;
|
|
33
|
+
o[k2] = m[k];
|
|
34
|
+
}));
|
|
35
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
36
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
37
|
+
}) : function(o, v) {
|
|
38
|
+
o["default"] = v;
|
|
39
|
+
});
|
|
40
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
41
|
+
var ownKeys = function(o) {
|
|
42
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
43
|
+
var ar = [];
|
|
44
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
45
|
+
return ar;
|
|
46
|
+
};
|
|
47
|
+
return ownKeys(o);
|
|
48
|
+
};
|
|
49
|
+
return function (mod) {
|
|
50
|
+
if (mod && mod.__esModule) return mod;
|
|
51
|
+
var result = {};
|
|
52
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
53
|
+
__setModuleDefault(result, mod);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
})();
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.buildRequestHash = buildRequestHash;
|
|
59
|
+
exports.sleep = sleep;
|
|
60
|
+
exports.idempotencyMiddleware = idempotencyMiddleware;
|
|
61
|
+
const crypto = __importStar(require("node:crypto"));
|
|
62
|
+
const idempotency_store_1 = require("./idempotency-store");
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Constants
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
const MAX_KEY_LENGTH = 255;
|
|
67
|
+
const PRINTABLE_ASCII_RE = /^[\x20-\x7e]+$/;
|
|
68
|
+
/** Maximum body size we will buffer and hash: 1 MiB */
|
|
69
|
+
const MAX_BODY_BYTES = 1 * 1024 * 1024;
|
|
70
|
+
/** Polling config for in-flight race resolution */
|
|
71
|
+
const IN_FLIGHT_POLL_ATTEMPTS = 3;
|
|
72
|
+
const IN_FLIGHT_POLL_INTERVAL_MS = 100;
|
|
73
|
+
// Headers replayed on cache hits (exclude security / hop-by-hop / CORS / auth
|
|
74
|
+
// headers which either need to be re-emitted fresh by the security-headers
|
|
75
|
+
// middleware on each call (HIGH-2 hardening), are session-scoped, or could
|
|
76
|
+
// leak credentials when replayed across callers).
|
|
77
|
+
const REPLAY_HEADER_DENYLIST = new Set([
|
|
78
|
+
"set-cookie",
|
|
79
|
+
"strict-transport-security",
|
|
80
|
+
"x-frame-options",
|
|
81
|
+
"x-content-type-options",
|
|
82
|
+
"referrer-policy",
|
|
83
|
+
"content-security-policy",
|
|
84
|
+
"permissions-policy",
|
|
85
|
+
"cross-origin-opener-policy",
|
|
86
|
+
"access-control-allow-origin",
|
|
87
|
+
"access-control-allow-credentials",
|
|
88
|
+
"access-control-allow-methods",
|
|
89
|
+
"access-control-allow-headers",
|
|
90
|
+
"transfer-encoding",
|
|
91
|
+
"connection",
|
|
92
|
+
// HIGH-2: never replay credential / authentication headers stored
|
|
93
|
+
// alongside a previous response. Each caller's auth context must be
|
|
94
|
+
// re-evaluated against their own credentials.
|
|
95
|
+
"authorization",
|
|
96
|
+
"www-authenticate",
|
|
97
|
+
"proxy-authenticate",
|
|
98
|
+
"x-csrf-token",
|
|
99
|
+
]);
|
|
100
|
+
/**
|
|
101
|
+
* Pull a tenant-scoped or user-scoped value out of the request context
|
|
102
|
+
* so the idempotency dedup key partitions cleanly across tenants
|
|
103
|
+
* (G4 HIGH-1). Federation POSTs all sit under `/api/tenants/{id}/...`,
|
|
104
|
+
* so the path-based extractor catches the common case; if an `auth`
|
|
105
|
+
* object was attached upstream we prefer its activeTenantId so callers
|
|
106
|
+
* acting through tenant-switch flows still partition correctly.
|
|
107
|
+
*/
|
|
108
|
+
function deriveScope(context) {
|
|
109
|
+
const ctxAuth = context.auth;
|
|
110
|
+
if (ctxAuth?.activeTenantId)
|
|
111
|
+
return `t:${ctxAuth.activeTenantId}`;
|
|
112
|
+
// Path-based fallback: /api/tenants/{tenantId}/...
|
|
113
|
+
const m = context.pathname.match(/^\/api\/tenants\/([^/]+)/);
|
|
114
|
+
if (m && m[1])
|
|
115
|
+
return `t:${m[1]}`;
|
|
116
|
+
if (ctxAuth?.userId)
|
|
117
|
+
return `u:${ctxAuth.userId}`;
|
|
118
|
+
return "anon";
|
|
119
|
+
}
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
// Error helpers
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
function jsonError(status, error, message, remediation) {
|
|
124
|
+
return new Response(JSON.stringify({ error, message, remediation }), {
|
|
125
|
+
status,
|
|
126
|
+
headers: { "content-type": "application/json" },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
// Hash helper
|
|
131
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
132
|
+
/**
|
|
133
|
+
* Build a deterministic hash over method + path + raw body bytes.
|
|
134
|
+
* Headers are intentionally excluded so that retries with different
|
|
135
|
+
* Idempotency-Key values but same intent hash the same way.
|
|
136
|
+
*/
|
|
137
|
+
function buildRequestHash(method, path, body) {
|
|
138
|
+
const hash = crypto.createHash("sha256");
|
|
139
|
+
hash.update(`${method.toUpperCase()}:${path}:`);
|
|
140
|
+
hash.update(body);
|
|
141
|
+
return hash.digest("hex");
|
|
142
|
+
}
|
|
143
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
144
|
+
// Sleep helper (mockable in tests)
|
|
145
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
function sleep(ms) {
|
|
147
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
148
|
+
}
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
// Middleware factory
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
/**
|
|
153
|
+
* @param storeOverride - Inject a mock store for testing; production uses
|
|
154
|
+
* DynamoIdempotencyStore built from `env.IDEMPOTENCY_TABLE`.
|
|
155
|
+
*/
|
|
156
|
+
function idempotencyMiddleware(storeOverride) {
|
|
157
|
+
return async (context, next) => {
|
|
158
|
+
const { request, env } = context;
|
|
159
|
+
const rawKey = request.headers.get("Idempotency-Key");
|
|
160
|
+
// ── No header → pass-through ──────────────────────────────────────────────
|
|
161
|
+
if (rawKey === null) {
|
|
162
|
+
return next();
|
|
163
|
+
}
|
|
164
|
+
// ── Only honour the header on POST ───────────────────────────────────────
|
|
165
|
+
if (request.method !== "POST") {
|
|
166
|
+
return next();
|
|
167
|
+
}
|
|
168
|
+
// ── Key validation ────────────────────────────────────────────────────────
|
|
169
|
+
if (rawKey.length === 0 || rawKey.length > MAX_KEY_LENGTH || !PRINTABLE_ASCII_RE.test(rawKey)) {
|
|
170
|
+
return jsonError(400, "IDEMPOTENCY_KEY_INVALID", `Idempotency-Key must be 1–${MAX_KEY_LENGTH} printable ASCII characters.`, "Generate a UUID v4 (e.g. crypto.randomUUID()) and use it as the Idempotency-Key.");
|
|
171
|
+
}
|
|
172
|
+
// ── Buffer + size-check the request body ──────────────────────────────────
|
|
173
|
+
let bodyBytes;
|
|
174
|
+
try {
|
|
175
|
+
bodyBytes = await readBodyWithLimit(request, MAX_BODY_BYTES);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return jsonError(413, "IDEMPOTENCY_BODY_TOO_LARGE", `Request body exceeds the ${MAX_BODY_BYTES / 1024 / 1024} MiB limit for idempotency processing.`, "Reduce the request body size or omit the Idempotency-Key header for large payloads.");
|
|
179
|
+
}
|
|
180
|
+
const requestHash = buildRequestHash(request.method, context.pathname, bodyBytes);
|
|
181
|
+
// ── Resolve the store ─────────────────────────────────────────────────────
|
|
182
|
+
const tableName = env.IDEMPOTENCY_TABLE;
|
|
183
|
+
const store = storeOverride ?? new idempotency_store_1.DynamoIdempotencyStore(tableName ?? `${env.STAGE ?? "dev"}-trellis-idempotency`);
|
|
184
|
+
// G4 HIGH-1: scope the dedup key by tenant (preferred) or user.
|
|
185
|
+
const scope = deriveScope(context);
|
|
186
|
+
const pk = (0, idempotency_store_1.buildPk)(rawKey, scope);
|
|
187
|
+
// ── Check existing record ─────────────────────────────────────────────────
|
|
188
|
+
const existing = await store.get(pk);
|
|
189
|
+
if (existing) {
|
|
190
|
+
// Hash mismatch → 422
|
|
191
|
+
if (existing.requestHash !== requestHash) {
|
|
192
|
+
return jsonError(422, "IDEMPOTENCY_KEY_REUSE", "This Idempotency-Key was used with a different request body.", "Use a fresh Idempotency-Key per logically distinct request.");
|
|
193
|
+
}
|
|
194
|
+
// In-flight: poll for the winner's response
|
|
195
|
+
if ((0, idempotency_store_1.isInFlight)(existing)) {
|
|
196
|
+
return await pollForResolution(store, pk, requestHash);
|
|
197
|
+
}
|
|
198
|
+
// Cached response → replay
|
|
199
|
+
return buildReplay(existing);
|
|
200
|
+
}
|
|
201
|
+
// ── Claim the key with a conditional write ────────────────────────────────
|
|
202
|
+
// G4 MEDIUM-5: write a SHORT-TTL sentinel here. If the worker dies
|
|
203
|
+
// between this claim and the resolve/delete step, the row evaporates
|
|
204
|
+
// on its own within ~60s and a retry can succeed. The full 24h TTL
|
|
205
|
+
// is only assigned after a successful resolve.
|
|
206
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
207
|
+
const inFlightRecord = {
|
|
208
|
+
pk,
|
|
209
|
+
requestHash,
|
|
210
|
+
responseStatus: 0,
|
|
211
|
+
responseBody: idempotency_store_1.IN_FLIGHT_SENTINEL,
|
|
212
|
+
responseHeaders: {},
|
|
213
|
+
expiresAt: nowSec + idempotency_store_1.IDEMPOTENCY_IN_FLIGHT_TTL_SECONDS,
|
|
214
|
+
};
|
|
215
|
+
const claimed = await store.putIfAbsent(inFlightRecord);
|
|
216
|
+
if (!claimed) {
|
|
217
|
+
// Lost the race — another worker claimed the key in the gap between
|
|
218
|
+
// our get() returning null and our putIfAbsent().
|
|
219
|
+
return await pollForResolution(store, pk, requestHash);
|
|
220
|
+
}
|
|
221
|
+
// ── We own the key: execute the handler ──────────────────────────────────
|
|
222
|
+
// We've consumed request.body. Patch context.request so downstream middleware
|
|
223
|
+
// and the route handler see a readable body again.
|
|
224
|
+
const freshRequest = new Request(request, { body: bodyBytes.length > 0 ? bodyBytes : null });
|
|
225
|
+
context.request = freshRequest;
|
|
226
|
+
let response;
|
|
227
|
+
try {
|
|
228
|
+
response = await next.call(undefined);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
// Handler threw — release the claim so retries aren't permanently blocked.
|
|
232
|
+
await store.delete(pk).catch(() => { });
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
// ── Persist the response (only on success 2xx/3xx) ───────────────────────
|
|
236
|
+
if (response.status < 400) {
|
|
237
|
+
const responseBodyText = await response.clone().text();
|
|
238
|
+
const headersToStore = {};
|
|
239
|
+
response.headers.forEach((value, name) => {
|
|
240
|
+
if (!REPLAY_HEADER_DENYLIST.has(name.toLowerCase())) {
|
|
241
|
+
headersToStore[name] = value;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
const resolvedRecord = {
|
|
245
|
+
pk,
|
|
246
|
+
requestHash,
|
|
247
|
+
responseStatus: response.status,
|
|
248
|
+
responseBody: responseBodyText,
|
|
249
|
+
responseHeaders: headersToStore,
|
|
250
|
+
expiresAt: nowSec + idempotency_store_1.IDEMPOTENCY_TTL_SECONDS,
|
|
251
|
+
};
|
|
252
|
+
await store.resolve(resolvedRecord).catch(() => { });
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Non-success: release the claim so the caller can retry with the same key.
|
|
256
|
+
await store.delete(pk).catch(() => { });
|
|
257
|
+
}
|
|
258
|
+
// ── Return original response with replay=false marker ────────────────────
|
|
259
|
+
const mutableResponse = new Response(response.body, {
|
|
260
|
+
status: response.status,
|
|
261
|
+
statusText: response.statusText,
|
|
262
|
+
headers: response.headers,
|
|
263
|
+
});
|
|
264
|
+
mutableResponse.headers.set("Idempotency-Replay", "false");
|
|
265
|
+
return mutableResponse;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
269
|
+
// Helpers
|
|
270
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
271
|
+
/**
|
|
272
|
+
* Strict Content-Length parser. Returns null when the header is absent,
|
|
273
|
+
* throws "body_too_large" when the value is malformed (G4 HIGH-3) or
|
|
274
|
+
* exceeds `maxBytes`. We reject anything that isn't a sequence of ASCII
|
|
275
|
+
* digits — `-1`, `1.0e10`, hex literals, etc. — because the looser
|
|
276
|
+
* parseInt(value, 10) silently absorbs them and the resulting limit
|
|
277
|
+
* check is bypassable.
|
|
278
|
+
*/
|
|
279
|
+
const STRICT_CONTENT_LENGTH_RE = /^[0-9]+$/;
|
|
280
|
+
/**
|
|
281
|
+
* Read the request body into a Uint8Array.
|
|
282
|
+
* Throws if the body exceeds `maxBytes`.
|
|
283
|
+
*/
|
|
284
|
+
async function readBodyWithLimit(request, maxBytes) {
|
|
285
|
+
const contentLength = request.headers.get("content-length");
|
|
286
|
+
if (contentLength !== null) {
|
|
287
|
+
if (!STRICT_CONTENT_LENGTH_RE.test(contentLength)) {
|
|
288
|
+
throw new Error("body_too_large");
|
|
289
|
+
}
|
|
290
|
+
const len = Number(contentLength);
|
|
291
|
+
if (!Number.isFinite(len) || len > maxBytes) {
|
|
292
|
+
throw new Error("body_too_large");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const reader = request.body?.getReader();
|
|
296
|
+
if (!reader) {
|
|
297
|
+
return new Uint8Array(0);
|
|
298
|
+
}
|
|
299
|
+
const chunks = [];
|
|
300
|
+
let totalBytes = 0;
|
|
301
|
+
while (true) {
|
|
302
|
+
const { done, value } = await reader.read();
|
|
303
|
+
if (done)
|
|
304
|
+
break;
|
|
305
|
+
if (value) {
|
|
306
|
+
totalBytes += value.byteLength;
|
|
307
|
+
if (totalBytes > maxBytes) {
|
|
308
|
+
reader.cancel().catch(() => { });
|
|
309
|
+
throw new Error("body_too_large");
|
|
310
|
+
}
|
|
311
|
+
chunks.push(value);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Concatenate chunks
|
|
315
|
+
const combined = new Uint8Array(totalBytes);
|
|
316
|
+
let offset = 0;
|
|
317
|
+
for (const chunk of chunks) {
|
|
318
|
+
combined.set(chunk, offset);
|
|
319
|
+
offset += chunk.byteLength;
|
|
320
|
+
}
|
|
321
|
+
return combined;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Poll the store for up to IN_FLIGHT_POLL_ATTEMPTS × IN_FLIGHT_POLL_INTERVAL_MS,
|
|
325
|
+
* waiting for an in-flight record to be resolved by the primary worker.
|
|
326
|
+
* Returns the cached response on resolution, or 409 on exhaustion.
|
|
327
|
+
*/
|
|
328
|
+
async function pollForResolution(store, pk, requestHash) {
|
|
329
|
+
for (let attempt = 0; attempt < IN_FLIGHT_POLL_ATTEMPTS; attempt++) {
|
|
330
|
+
await sleep(IN_FLIGHT_POLL_INTERVAL_MS);
|
|
331
|
+
const record = await store.get(pk);
|
|
332
|
+
if (!record) {
|
|
333
|
+
// Primary failed and deleted the claim — treat as if no record exists.
|
|
334
|
+
// Caller will retry; for now return 409 to avoid re-running the handler.
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
if (record.requestHash !== requestHash) {
|
|
338
|
+
return jsonError(422, "IDEMPOTENCY_KEY_REUSE", "This Idempotency-Key was used with a different request body.", "Use a fresh Idempotency-Key per logically distinct request.");
|
|
339
|
+
}
|
|
340
|
+
if (!(0, idempotency_store_1.isInFlight)(record)) {
|
|
341
|
+
return buildReplay(record);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return jsonError(409, "IDEMPOTENCY_KEY_IN_FLIGHT", "Another request with this Idempotency-Key is still being processed.", "Wait a moment and retry, or use a new Idempotency-Key.");
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Reconstruct a Response from a cached IdempotencyRecord and add replay header.
|
|
348
|
+
*/
|
|
349
|
+
function buildReplay(record) {
|
|
350
|
+
const headers = new Headers(record.responseHeaders);
|
|
351
|
+
headers.set("Idempotency-Replay", "true");
|
|
352
|
+
headers.set("content-type", headers.get("content-type") ?? "application/json");
|
|
353
|
+
return new Response(record.responseBody, {
|
|
354
|
+
status: record.responseStatus,
|
|
355
|
+
headers,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
//# sourceMappingURL=idempotency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../../src/lib/middleware/idempotency.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trusted client-IP derivation (G4 CRITICAL-3, LOW-1, LOW-3).
|
|
3
|
+
*
|
|
4
|
+
* The previous pattern — `X-Forwarded-For.split(",")[0]` plus
|
|
5
|
+
* `CF-Connecting-IP` — assumed a Cloudflare-class trusted edge sitting
|
|
6
|
+
* in front of trellis. Behind ALB the leftmost XFF entry is supplied
|
|
7
|
+
* by the immediate client and is therefore caller-supplied input. This
|
|
8
|
+
* helper centralises IP derivation behind an explicit `TRUSTED_PROXY`
|
|
9
|
+
* env flag so the rule is the same for every call site.
|
|
10
|
+
*
|
|
11
|
+
* Configuration:
|
|
12
|
+
* TRUSTED_PROXY=none — default. Do not honour XFF or CF
|
|
13
|
+
* headers. Returns the connection's
|
|
14
|
+
* remote address only (or "unknown" if
|
|
15
|
+
* the runtime does not expose one).
|
|
16
|
+
* TRUSTED_PROXY=alb — application is behind a trusted ALB.
|
|
17
|
+
* Parse XFF taking the right-most
|
|
18
|
+
* appended hop (the ALB's view of the
|
|
19
|
+
* client) rather than the leftmost.
|
|
20
|
+
* TRUSTED_PROXY=cloudflare — application is fronted by Cloudflare.
|
|
21
|
+
* Trust CF-Connecting-IP.
|
|
22
|
+
*
|
|
23
|
+
* The result is always validated against an IPv4/IPv6 shape regex.
|
|
24
|
+
* Anything that does not match returns the literal string "unknown" so
|
|
25
|
+
* downstream rate-limit keys cannot be poisoned with arbitrary content.
|
|
26
|
+
*/
|
|
27
|
+
interface RemoteAddrEnv {
|
|
28
|
+
TRUSTED_PROXY?: string;
|
|
29
|
+
}
|
|
30
|
+
/** True when `s` parses as an IPv4 dotted-quad or an IPv6 shape. */
|
|
31
|
+
export declare function isIpShape(s: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Derive a client IP we are willing to use for rate-limit keys / audit
|
|
34
|
+
* payloads. Always returns a non-empty string; "unknown" when nothing
|
|
35
|
+
* trustworthy is available.
|
|
36
|
+
*/
|
|
37
|
+
export declare function trustedClientIp(request: Request, env: RemoteAddrEnv): string;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=trusted-client-ip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trusted-client-ip.d.ts","sourceRoot":"","sources":["../../../src/lib/net/trusted-client-ip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,UAAU,aAAa;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAiBD,oEAAoE;AACpE,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAM5C;AAcD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,aAAa,GAAG,MAAM,CA4B5E"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Trusted client-IP derivation (G4 CRITICAL-3, LOW-1, LOW-3).
|
|
4
|
+
*
|
|
5
|
+
* The previous pattern — `X-Forwarded-For.split(",")[0]` plus
|
|
6
|
+
* `CF-Connecting-IP` — assumed a Cloudflare-class trusted edge sitting
|
|
7
|
+
* in front of trellis. Behind ALB the leftmost XFF entry is supplied
|
|
8
|
+
* by the immediate client and is therefore caller-supplied input. This
|
|
9
|
+
* helper centralises IP derivation behind an explicit `TRUSTED_PROXY`
|
|
10
|
+
* env flag so the rule is the same for every call site.
|
|
11
|
+
*
|
|
12
|
+
* Configuration:
|
|
13
|
+
* TRUSTED_PROXY=none — default. Do not honour XFF or CF
|
|
14
|
+
* headers. Returns the connection's
|
|
15
|
+
* remote address only (or "unknown" if
|
|
16
|
+
* the runtime does not expose one).
|
|
17
|
+
* TRUSTED_PROXY=alb — application is behind a trusted ALB.
|
|
18
|
+
* Parse XFF taking the right-most
|
|
19
|
+
* appended hop (the ALB's view of the
|
|
20
|
+
* client) rather than the leftmost.
|
|
21
|
+
* TRUSTED_PROXY=cloudflare — application is fronted by Cloudflare.
|
|
22
|
+
* Trust CF-Connecting-IP.
|
|
23
|
+
*
|
|
24
|
+
* The result is always validated against an IPv4/IPv6 shape regex.
|
|
25
|
+
* Anything that does not match returns the literal string "unknown" so
|
|
26
|
+
* downstream rate-limit keys cannot be poisoned with arbitrary content.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.isIpShape = isIpShape;
|
|
30
|
+
exports.trustedClientIp = trustedClientIp;
|
|
31
|
+
/**
|
|
32
|
+
* IPv4 dotted-quad: each octet 0-255. Permissive enough to pass any
|
|
33
|
+
* real-world value while rejecting obvious garbage (newlines, commas,
|
|
34
|
+
* SQL fragments, etc.).
|
|
35
|
+
*/
|
|
36
|
+
const IPV4_RE = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
37
|
+
/**
|
|
38
|
+
* IPv6 shape check. Intentionally loose — accepts compressed and
|
|
39
|
+
* uncompressed forms, IPv4-mapped, scope IDs — but rejects anything
|
|
40
|
+
* containing characters outside the IPv6 alphabet.
|
|
41
|
+
*/
|
|
42
|
+
const IPV6_RE = /^[0-9a-fA-F:.%]+$/;
|
|
43
|
+
/** True when `s` parses as an IPv4 dotted-quad or an IPv6 shape. */
|
|
44
|
+
function isIpShape(s) {
|
|
45
|
+
if (!s)
|
|
46
|
+
return false;
|
|
47
|
+
if (s.length > 64)
|
|
48
|
+
return false;
|
|
49
|
+
if (IPV4_RE.test(s))
|
|
50
|
+
return true;
|
|
51
|
+
if (s.includes(":") && IPV6_RE.test(s))
|
|
52
|
+
return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/** Read a remote-address hint from a `Request` if the runtime exposes one. */
|
|
56
|
+
function remoteAddrFromRequest(request) {
|
|
57
|
+
// Node 22+ behind Express may attach the socket via this property.
|
|
58
|
+
// Web/Cloudflare runtimes do not. Treat anything else as absent.
|
|
59
|
+
const candidate = request
|
|
60
|
+
.socket?.remoteAddress;
|
|
61
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Derive a client IP we are willing to use for rate-limit keys / audit
|
|
68
|
+
* payloads. Always returns a non-empty string; "unknown" when nothing
|
|
69
|
+
* trustworthy is available.
|
|
70
|
+
*/
|
|
71
|
+
function trustedClientIp(request, env) {
|
|
72
|
+
const mode = (env.TRUSTED_PROXY ?? "none").toLowerCase();
|
|
73
|
+
if (mode === "cloudflare") {
|
|
74
|
+
const cf = request.headers.get("CF-Connecting-IP");
|
|
75
|
+
if (cf && isIpShape(cf.trim()))
|
|
76
|
+
return cf.trim();
|
|
77
|
+
return "unknown";
|
|
78
|
+
}
|
|
79
|
+
if (mode === "alb") {
|
|
80
|
+
// ALB appends the immediate client to XFF. The right-most entry is
|
|
81
|
+
// therefore the value the ALB itself observed, which is the closest
|
|
82
|
+
// we can get to the real source address while behind a single
|
|
83
|
+
// trusted proxy hop.
|
|
84
|
+
const xff = request.headers.get("X-Forwarded-For");
|
|
85
|
+
if (xff) {
|
|
86
|
+
const parts = xff.split(",").map((p) => p.trim()).filter(Boolean);
|
|
87
|
+
const candidate = parts[parts.length - 1];
|
|
88
|
+
if (candidate && isIpShape(candidate))
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
return "unknown";
|
|
92
|
+
}
|
|
93
|
+
// mode === "none" (default). Do not trust XFF or CF headers. Use the
|
|
94
|
+
// connection's remote address only when the runtime exposes one.
|
|
95
|
+
const remote = remoteAddrFromRequest(request);
|
|
96
|
+
if (remote && isIpShape(remote))
|
|
97
|
+
return remote;
|
|
98
|
+
return "unknown";
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=trusted-client-ip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trusted-client-ip.js","sourceRoot":"","sources":["../../../src/lib/net/trusted-client-ip.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;AAsBH,8BAMC;AAmBD,0CA4BC;AArED;;;;GAIG;AACH,MAAM,OAAO,GACX,6EAA6E,CAAC;AAEhF;;;;GAIG;AACH,MAAM,OAAO,GAAG,mBAAmB,CAAC;AAEpC,oEAAoE;AACpE,SAAgB,SAAS,CAAC,CAAS;IACjC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACrB,IAAI,CAAC,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,KAAK,CAAC;IAChC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,8EAA8E;AAC9E,SAAS,qBAAqB,CAAC,OAAgB;IAC7C,mEAAmE;IACnE,iEAAiE;IACjE,MAAM,SAAS,GAAI,OAA8D;SAC9E,MAAM,EAAE,aAAa,CAAC;IACzB,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1D,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe,CAAC,OAAgB,EAAE,GAAkB;IAClE,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,aAAa,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IAEzD,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;QAC1B,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACnD,IAAI,EAAE,IAAI,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;YAAE,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC;QACjD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,mEAAmE;QACnE,oEAAoE;QACpE,8DAA8D;QAC9D,qBAAqB;QACrB,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QACnD,IAAI,GAAG,EAAE,CAAC;YACR,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAClE,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC1C,IAAI,SAAS,IAAI,SAAS,CAAC,SAAS,CAAC;gBAAE,OAAO,SAAS,CAAC;QAC1D,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,qEAAqE;IACrE,iEAAiE;IACjE,MAAM,MAAM,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,MAAM,IAAI,SAAS,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAC/C,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -26,27 +26,27 @@ export declare class NotificationHandler {
|
|
|
26
26
|
* Create a notification for a user.
|
|
27
27
|
* Checks preferences (unless SAFETY_ALERT or PARENTAL_LINK) and quiet hours.
|
|
28
28
|
*/
|
|
29
|
-
createNotification(userId: string, type: NotificationType, title: string, body: string, data: any, env: Env): Promise<{
|
|
29
|
+
createNotification(userId: string, type: NotificationType, title: string, body: string, data: any, env: Env, tenantId: string): Promise<{
|
|
30
30
|
id: string;
|
|
31
31
|
}>;
|
|
32
32
|
/**
|
|
33
33
|
* Get paginated notifications for a user.
|
|
34
34
|
*/
|
|
35
|
-
getNotifications(userId: string, cursor: string | null, limit: number, env: Env): Promise<NotificationListResponse>;
|
|
35
|
+
getNotifications(userId: string, cursor: string | null, limit: number, env: Env, tenantId: string): Promise<NotificationListResponse>;
|
|
36
36
|
/**
|
|
37
37
|
* Mark a single notification as read. Verifies it belongs to the user.
|
|
38
38
|
*/
|
|
39
|
-
markRead(userId: string, notificationId: string, env: Env): Promise<void>;
|
|
39
|
+
markRead(userId: string, notificationId: string, env: Env, tenantId: string): Promise<void>;
|
|
40
40
|
/**
|
|
41
41
|
* Mark all unread notifications as read for a user.
|
|
42
42
|
*/
|
|
43
|
-
markAllRead(userId: string, env: Env): Promise<void>;
|
|
43
|
+
markAllRead(userId: string, env: Env, tenantId: string): Promise<void>;
|
|
44
44
|
/**
|
|
45
45
|
* Get unread notification count.
|
|
46
46
|
* CHILD/TEEN: return { hasUnread: boolean } only (no exact count).
|
|
47
47
|
* ADULT: return { hasUnread: boolean, count: number }.
|
|
48
48
|
*/
|
|
49
|
-
getUnreadCount(userId: string, ageTier: string, env: Env): Promise<{
|
|
49
|
+
getUnreadCount(userId: string, ageTier: string, env: Env, tenantId: string): Promise<{
|
|
50
50
|
hasUnread: boolean;
|
|
51
51
|
count?: number;
|
|
52
52
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-handler.d.ts","sourceRoot":"","sources":["../../src/lib/notification-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAGlC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIvD,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,KAAK,CAAC;QACnB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,gBAAgB,CAAC;QACvB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,GAAG,CAAC;QACV,IAAI,EAAE,OAAO,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAQD,qBAAa,mBAAmB;IAC9B;;;OAGG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,EACT,GAAG,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"notification-handler.d.ts","sourceRoot":"","sources":["../../src/lib/notification-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAGlC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIvD,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,KAAK,CAAC;QACnB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,gBAAgB,CAAC;QACvB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,GAAG,CAAC;QACV,IAAI,EAAE,OAAO,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAQD,qBAAa,mBAAmB;IAC9B;;;OAGG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,EACT,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAmE1B;;OAEG;IACG,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,CAAC;IAuCpC;;OAEG;IACG,QAAQ,CACZ,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAqBhB;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5E;;;;OAIG;IACG,cAAc,CAClB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAwBlD;;OAEG;IACH,OAAO,CAAC,aAAa;IAgCrB;;OAEG;IACH,OAAO,CAAC,cAAc;CA0BvB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,cAAc,EAAE,MAAM;CAInC"}
|
|
@@ -21,7 +21,7 @@ class NotificationHandler {
|
|
|
21
21
|
* Create a notification for a user.
|
|
22
22
|
* Checks preferences (unless SAFETY_ALERT or PARENTAL_LINK) and quiet hours.
|
|
23
23
|
*/
|
|
24
|
-
async createNotification(userId, type, title, body, data, env) {
|
|
24
|
+
async createNotification(userId, type, title, body, data, env, tenantId) {
|
|
25
25
|
const logger = logger_1.Logger.getInstance(env);
|
|
26
26
|
const db = (0, db_1.createPrisma)(env);
|
|
27
27
|
try {
|
|
@@ -68,6 +68,7 @@ class NotificationHandler {
|
|
|
68
68
|
body,
|
|
69
69
|
data: data ?? undefined,
|
|
70
70
|
deliveredAt,
|
|
71
|
+
tenantId,
|
|
71
72
|
},
|
|
72
73
|
});
|
|
73
74
|
return { id: notification.id };
|
|
@@ -83,13 +84,14 @@ class NotificationHandler {
|
|
|
83
84
|
/**
|
|
84
85
|
* Get paginated notifications for a user.
|
|
85
86
|
*/
|
|
86
|
-
async getNotifications(userId, cursor, limit, env) {
|
|
87
|
+
async getNotifications(userId, cursor, limit, env, tenantId) {
|
|
87
88
|
const db = (0, db_1.createPrisma)(env);
|
|
88
89
|
try {
|
|
89
90
|
const safeLimit = Math.min(Math.max(limit, 1), 50);
|
|
90
91
|
const notifications = await db.notification.findMany({
|
|
91
92
|
where: {
|
|
92
93
|
userId,
|
|
94
|
+
tenantId,
|
|
93
95
|
...(cursor ? { createdAt: { lt: new Date(cursor) } } : {}),
|
|
94
96
|
},
|
|
95
97
|
orderBy: { createdAt: "desc" },
|
|
@@ -120,11 +122,11 @@ class NotificationHandler {
|
|
|
120
122
|
/**
|
|
121
123
|
* Mark a single notification as read. Verifies it belongs to the user.
|
|
122
124
|
*/
|
|
123
|
-
async markRead(userId, notificationId, env) {
|
|
125
|
+
async markRead(userId, notificationId, env, tenantId) {
|
|
124
126
|
const db = (0, db_1.createPrisma)(env);
|
|
125
127
|
try {
|
|
126
128
|
const notification = await db.notification.findFirst({
|
|
127
|
-
where: { id: notificationId, userId },
|
|
129
|
+
where: { id: notificationId, userId, tenantId },
|
|
128
130
|
});
|
|
129
131
|
if (!notification) {
|
|
130
132
|
throw new NotificationNotFoundError(notificationId);
|
|
@@ -141,11 +143,11 @@ class NotificationHandler {
|
|
|
141
143
|
/**
|
|
142
144
|
* Mark all unread notifications as read for a user.
|
|
143
145
|
*/
|
|
144
|
-
async markAllRead(userId, env) {
|
|
146
|
+
async markAllRead(userId, env, tenantId) {
|
|
145
147
|
const db = (0, db_1.createPrisma)(env);
|
|
146
148
|
try {
|
|
147
149
|
await db.notification.updateMany({
|
|
148
|
-
where: { userId, read: false },
|
|
150
|
+
where: { userId, tenantId, read: false },
|
|
149
151
|
data: { read: true },
|
|
150
152
|
});
|
|
151
153
|
}
|
|
@@ -158,20 +160,20 @@ class NotificationHandler {
|
|
|
158
160
|
* CHILD/TEEN: return { hasUnread: boolean } only (no exact count).
|
|
159
161
|
* ADULT: return { hasUnread: boolean, count: number }.
|
|
160
162
|
*/
|
|
161
|
-
async getUnreadCount(userId, ageTier, env) {
|
|
163
|
+
async getUnreadCount(userId, ageTier, env, tenantId) {
|
|
162
164
|
const db = (0, db_1.createPrisma)(env);
|
|
163
165
|
try {
|
|
164
166
|
if (ageTier === "CHILD" || ageTier === "TEEN") {
|
|
165
167
|
// For minors, only check existence (no exact count)
|
|
166
168
|
const first = await db.notification.findFirst({
|
|
167
|
-
where: { userId, read: false },
|
|
169
|
+
where: { userId, tenantId, read: false },
|
|
168
170
|
select: { id: true },
|
|
169
171
|
});
|
|
170
172
|
return { hasUnread: !!first };
|
|
171
173
|
}
|
|
172
174
|
// ADULT: return exact count
|
|
173
175
|
const count = await db.notification.count({
|
|
174
|
-
where: { userId, read: false },
|
|
176
|
+
where: { userId, tenantId, read: false },
|
|
175
177
|
});
|
|
176
178
|
return { hasUnread: count > 0, count };
|
|
177
179
|
}
|