@adonis-agora/flags 0.0.0-beta-20261112141227
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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/assets/spa/assets/index-DdmFo7xr.js +12 -0
- package/dist/assets/spa/assets/index-DdmFo7xr.js.map +1 -0
- package/dist/assets/spa/assets/index-DhkcP3Cw.css +2 -0
- package/dist/assets/spa/index.html +26 -0
- package/dist/commands/flags_list.d.ts +9 -0
- package/dist/commands/flags_list.d.ts.map +1 -0
- package/dist/commands/flags_list.js +51 -0
- package/dist/commands/flags_list.js.map +1 -0
- package/dist/commands/flags_ops.d.ts +60 -0
- package/dist/commands/flags_ops.d.ts.map +1 -0
- package/dist/commands/flags_ops.js +253 -0
- package/dist/commands/flags_ops.js.map +1 -0
- package/dist/commands/flags_refs.d.ts +15 -0
- package/dist/commands/flags_refs.d.ts.map +1 -0
- package/dist/commands/flags_refs.js +90 -0
- package/dist/commands/flags_refs.js.map +1 -0
- package/dist/commands/flags_set.d.ts +19 -0
- package/dist/commands/flags_set.d.ts.map +1 -0
- package/dist/commands/flags_set.js +97 -0
- package/dist/commands/flags_set.js.map +1 -0
- package/dist/commands/flags_types.d.ts +18 -0
- package/dist/commands/flags_types.d.ts.map +1 -0
- package/dist/commands/flags_types.js +94 -0
- package/dist/commands/flags_types.js.map +1 -0
- package/dist/commands/flags_work.d.ts +16 -0
- package/dist/commands/flags_work.d.ts.map +1 -0
- package/dist/commands/flags_work.js +71 -0
- package/dist/commands/flags_work.js.map +1 -0
- package/dist/commands/main.d.ts +15 -0
- package/dist/commands/main.d.ts.map +1 -0
- package/dist/commands/main.js +30 -0
- package/dist/commands/main.js.map +1 -0
- package/dist/commands/shared.d.ts +21 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +70 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/configure.d.ts +11 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +32 -0
- package/dist/configure.js.map +1 -0
- package/dist/providers/dashboard_provider.d.ts +65 -0
- package/dist/providers/dashboard_provider.d.ts.map +1 -0
- package/dist/providers/dashboard_provider.js +340 -0
- package/dist/providers/dashboard_provider.js.map +1 -0
- package/dist/providers/flags_provider.d.ts +30 -0
- package/dist/providers/flags_provider.d.ts.map +1 -0
- package/dist/providers/flags_provider.js +184 -0
- package/dist/providers/flags_provider.js.map +1 -0
- package/dist/services/main.d.ts +17 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +20 -0
- package/dist/services/main.js.map +1 -0
- package/dist/src/admin.d.ts +189 -0
- package/dist/src/admin.d.ts.map +1 -0
- package/dist/src/admin.js +539 -0
- package/dist/src/admin.js.map +1 -0
- package/dist/src/analytics/backend.d.ts +18 -0
- package/dist/src/analytics/backend.d.ts.map +1 -0
- package/dist/src/analytics/backend.js +25 -0
- package/dist/src/analytics/backend.js.map +1 -0
- package/dist/src/analytics/clickhouse_backend.d.ts +16 -0
- package/dist/src/analytics/clickhouse_backend.d.ts.map +1 -0
- package/dist/src/analytics/clickhouse_backend.js +48 -0
- package/dist/src/analytics/clickhouse_backend.js.map +1 -0
- package/dist/src/analytics/duckdb.d.ts +39 -0
- package/dist/src/analytics/duckdb.d.ts.map +1 -0
- package/dist/src/analytics/duckdb.js +117 -0
- package/dist/src/analytics/duckdb.js.map +1 -0
- package/dist/src/analytics/lucid_backend.d.ts +16 -0
- package/dist/src/analytics/lucid_backend.d.ts.map +1 -0
- package/dist/src/analytics/lucid_backend.js +45 -0
- package/dist/src/analytics/lucid_backend.js.map +1 -0
- package/dist/src/analytics/metrics.d.ts +50 -0
- package/dist/src/analytics/metrics.d.ts.map +1 -0
- package/dist/src/analytics/metrics.js +42 -0
- package/dist/src/analytics/metrics.js.map +1 -0
- package/dist/src/analytics/results.d.ts +80 -0
- package/dist/src/analytics/results.d.ts.map +1 -0
- package/dist/src/analytics/results.js +143 -0
- package/dist/src/analytics/results.js.map +1 -0
- package/dist/src/dashboard/access_denied_page.d.ts +100 -0
- package/dist/src/dashboard/access_denied_page.d.ts.map +1 -0
- package/dist/src/dashboard/access_denied_page.js +262 -0
- package/dist/src/dashboard/access_denied_page.js.map +1 -0
- package/dist/src/dashboard/actor.d.ts +12 -0
- package/dist/src/dashboard/actor.d.ts.map +1 -0
- package/dist/src/dashboard/actor.js +33 -0
- package/dist/src/dashboard/actor.js.map +1 -0
- package/dist/src/dashboard/authorize_decision.d.ts +48 -0
- package/dist/src/dashboard/authorize_decision.d.ts.map +1 -0
- package/dist/src/dashboard/authorize_decision.js +44 -0
- package/dist/src/dashboard/authorize_decision.js.map +1 -0
- package/dist/src/dashboard/console_authorize.d.ts +77 -0
- package/dist/src/dashboard/console_authorize.d.ts.map +1 -0
- package/dist/src/dashboard/console_authorize.js +104 -0
- package/dist/src/dashboard/console_authorize.js.map +1 -0
- package/dist/src/dashboard/define_config.d.ts +71 -0
- package/dist/src/dashboard/define_config.d.ts.map +1 -0
- package/dist/src/dashboard/define_config.js +18 -0
- package/dist/src/dashboard/define_config.js.map +1 -0
- package/dist/src/dashboard/handlers.d.ts +172 -0
- package/dist/src/dashboard/handlers.d.ts.map +1 -0
- package/dist/src/dashboard/handlers.js +949 -0
- package/dist/src/dashboard/handlers.js.map +1 -0
- package/dist/src/dashboard/index.d.ts +15 -0
- package/dist/src/dashboard/index.d.ts.map +1 -0
- package/dist/src/dashboard/index.js +10 -0
- package/dist/src/dashboard/index.js.map +1 -0
- package/dist/src/dashboard/shape.d.ts +12 -0
- package/dist/src/dashboard/shape.d.ts.map +1 -0
- package/dist/src/dashboard/shape.js +232 -0
- package/dist/src/dashboard/shape.js.map +1 -0
- package/dist/src/dashboard/spa.d.ts +34 -0
- package/dist/src/dashboard/spa.d.ts.map +1 -0
- package/dist/src/dashboard/spa.js +50 -0
- package/dist/src/dashboard/spa.js.map +1 -0
- package/dist/src/declare.d.ts +85 -0
- package/dist/src/declare.d.ts.map +1 -0
- package/dist/src/declare.js +62 -0
- package/dist/src/declare.js.map +1 -0
- package/dist/src/define_config.d.ts +256 -0
- package/dist/src/define_config.d.ts.map +1 -0
- package/dist/src/define_config.js +90 -0
- package/dist/src/define_config.js.map +1 -0
- package/dist/src/durable.d.ts +29 -0
- package/dist/src/durable.d.ts.map +1 -0
- package/dist/src/durable.js +55 -0
- package/dist/src/durable.js.map +1 -0
- package/dist/src/enrichers.d.ts +51 -0
- package/dist/src/enrichers.d.ts.map +1 -0
- package/dist/src/enrichers.js +105 -0
- package/dist/src/enrichers.js.map +1 -0
- package/dist/src/events/buffer.d.ts +38 -0
- package/dist/src/events/buffer.d.ts.map +1 -0
- package/dist/src/events/buffer.js +86 -0
- package/dist/src/events/buffer.js.map +1 -0
- package/dist/src/events/event.d.ts +42 -0
- package/dist/src/events/event.d.ts.map +1 -0
- package/dist/src/events/event.js +82 -0
- package/dist/src/events/event.js.map +1 -0
- package/dist/src/http/browser.d.ts +33 -0
- package/dist/src/http/browser.d.ts.map +1 -0
- package/dist/src/http/browser.js +81 -0
- package/dist/src/http/browser.js.map +1 -0
- package/dist/src/http/browser_api.d.ts +48 -0
- package/dist/src/http/browser_api.d.ts.map +1 -0
- package/dist/src/http/browser_api.js +273 -0
- package/dist/src/http/browser_api.js.map +1 -0
- package/dist/src/http/browser_tokens.d.ts +27 -0
- package/dist/src/http/browser_tokens.d.ts.map +1 -0
- package/dist/src/http/browser_tokens.js +48 -0
- package/dist/src/http/browser_tokens.js.map +1 -0
- package/dist/src/http/control_plane.d.ts +58 -0
- package/dist/src/http/control_plane.d.ts.map +1 -0
- package/dist/src/http/control_plane.js +318 -0
- package/dist/src/http/control_plane.js.map +1 -0
- package/dist/src/http/csrf.d.ts +22 -0
- package/dist/src/http/csrf.d.ts.map +1 -0
- package/dist/src/http/csrf.js +24 -0
- package/dist/src/http/csrf.js.map +1 -0
- package/dist/src/http/edge_tag.d.ts +55 -0
- package/dist/src/http/edge_tag.d.ts.map +1 -0
- package/dist/src/http/edge_tag.js +45 -0
- package/dist/src/http/edge_tag.js.map +1 -0
- package/dist/src/http/flag_middleware.d.ts +27 -0
- package/dist/src/http/flag_middleware.d.ts.map +1 -0
- package/dist/src/http/flag_middleware.js +30 -0
- package/dist/src/http/flag_middleware.js.map +1 -0
- package/dist/src/http/flags_middleware.d.ts +20 -0
- package/dist/src/http/flags_middleware.d.ts.map +1 -0
- package/dist/src/http/flags_middleware.js +20 -0
- package/dist/src/http/flags_middleware.js.map +1 -0
- package/dist/src/http/subject_resolver.d.ts +45 -0
- package/dist/src/http/subject_resolver.d.ts.map +1 -0
- package/dist/src/http/subject_resolver.js +93 -0
- package/dist/src/http/subject_resolver.js.map +1 -0
- package/dist/src/identity.d.ts +82 -0
- package/dist/src/identity.d.ts.map +1 -0
- package/dist/src/identity.js +100 -0
- package/dist/src/identity.js.map +1 -0
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +34 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/manager.d.ts +154 -0
- package/dist/src/manager.d.ts.map +1 -0
- package/dist/src/manager.js +333 -0
- package/dist/src/manager.js.map +1 -0
- package/dist/src/openfeature/provider.d.ts +36 -0
- package/dist/src/openfeature/provider.d.ts.map +1 -0
- package/dist/src/openfeature/provider.js +82 -0
- package/dist/src/openfeature/provider.js.map +1 -0
- package/dist/src/relay/relay.d.ts +36 -0
- package/dist/src/relay/relay.d.ts.map +1 -0
- package/dist/src/relay/relay.js +73 -0
- package/dist/src/relay/relay.js.map +1 -0
- package/dist/src/relay/results_cache.d.ts +20 -0
- package/dist/src/relay/results_cache.d.ts.map +1 -0
- package/dist/src/relay/results_cache.js +31 -0
- package/dist/src/relay/results_cache.js.map +1 -0
- package/dist/src/relay/worker.d.ts +66 -0
- package/dist/src/relay/worker.d.ts.map +1 -0
- package/dist/src/relay/worker.js +177 -0
- package/dist/src/relay/worker.js.map +1 -0
- package/dist/src/ruleset_source.d.ts +73 -0
- package/dist/src/ruleset_source.d.ts.map +1 -0
- package/dist/src/ruleset_source.js +128 -0
- package/dist/src/ruleset_source.js.map +1 -0
- package/dist/src/runtime.d.ts +70 -0
- package/dist/src/runtime.d.ts.map +1 -0
- package/dist/src/runtime.js +301 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/schema.d.ts +39 -0
- package/dist/src/schema.d.ts.map +1 -0
- package/dist/src/schema.js +311 -0
- package/dist/src/schema.js.map +1 -0
- package/dist/src/services/accessor.d.ts +5 -0
- package/dist/src/services/accessor.d.ts.map +1 -0
- package/dist/src/services/accessor.js +20 -0
- package/dist/src/services/accessor.js.map +1 -0
- package/dist/src/sinks/clickhouse.d.ts +41 -0
- package/dist/src/sinks/clickhouse.d.ts.map +1 -0
- package/dist/src/sinks/clickhouse.js +117 -0
- package/dist/src/sinks/clickhouse.js.map +1 -0
- package/dist/src/sinks/lucid_sink.d.ts +25 -0
- package/dist/src/sinks/lucid_sink.d.ts.map +1 -0
- package/dist/src/sinks/lucid_sink.js +87 -0
- package/dist/src/sinks/lucid_sink.js.map +1 -0
- package/dist/src/sinks/parquet_sink.d.ts +41 -0
- package/dist/src/sinks/parquet_sink.d.ts.map +1 -0
- package/dist/src/sinks/parquet_sink.js +140 -0
- package/dist/src/sinks/parquet_sink.js.map +1 -0
- package/dist/src/sinks/posthog_sink.d.ts +29 -0
- package/dist/src/sinks/posthog_sink.d.ts.map +1 -0
- package/dist/src/sinks/posthog_sink.js +65 -0
- package/dist/src/sinks/posthog_sink.js.map +1 -0
- package/dist/src/sinks/sink.d.ts +36 -0
- package/dist/src/sinks/sink.d.ts.map +1 -0
- package/dist/src/sinks/sink.js +36 -0
- package/dist/src/sinks/sink.js.map +1 -0
- package/dist/src/sql.d.ts +33 -0
- package/dist/src/sql.d.ts.map +1 -0
- package/dist/src/sql.js +66 -0
- package/dist/src/sql.js.map +1 -0
- package/dist/src/stores/lucid_store.d.ts +55 -0
- package/dist/src/stores/lucid_store.d.ts.map +1 -0
- package/dist/src/stores/lucid_store.js +412 -0
- package/dist/src/stores/lucid_store.js.map +1 -0
- package/dist/src/stores/memory_store.d.ts +38 -0
- package/dist/src/stores/memory_store.d.ts.map +1 -0
- package/dist/src/stores/memory_store.js +166 -0
- package/dist/src/stores/memory_store.js.map +1 -0
- package/dist/src/stores/store.d.ts +166 -0
- package/dist/src/stores/store.d.ts.map +1 -0
- package/dist/src/stores/store.js +67 -0
- package/dist/src/stores/store.js.map +1 -0
- package/dist/src/subject.d.ts +25 -0
- package/dist/src/subject.d.ts.map +1 -0
- package/dist/src/subject.js +30 -0
- package/dist/src/subject.js.map +1 -0
- package/dist/src/testing/index.d.ts +31 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +60 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/topology/browser_token.d.ts +48 -0
- package/dist/src/topology/browser_token.d.ts.map +1 -0
- package/dist/src/topology/browser_token.js +128 -0
- package/dist/src/topology/browser_token.js.map +1 -0
- package/dist/src/topology/remote_source.d.ts +39 -0
- package/dist/src/topology/remote_source.d.ts.map +1 -0
- package/dist/src/topology/remote_source.js +184 -0
- package/dist/src/topology/remote_source.js.map +1 -0
- package/dist/src/topology/token.d.ts +36 -0
- package/dist/src/topology/token.d.ts.map +1 -0
- package/dist/src/topology/token.js +68 -0
- package/dist/src/topology/token.js.map +1 -0
- package/dist/src/topology/verify.d.ts +88 -0
- package/dist/src/topology/verify.d.ts.map +1 -0
- package/dist/src/topology/verify.js +128 -0
- package/dist/src/topology/verify.js.map +1 -0
- package/dist/src/transports/db_transport.d.ts +55 -0
- package/dist/src/transports/db_transport.d.ts.map +1 -0
- package/dist/src/transports/db_transport.js +202 -0
- package/dist/src/transports/db_transport.js.map +1 -0
- package/dist/src/transports/http_transport.d.ts +30 -0
- package/dist/src/transports/http_transport.d.ts.map +1 -0
- package/dist/src/transports/http_transport.js +51 -0
- package/dist/src/transports/http_transport.js.map +1 -0
- package/dist/src/transports/memory_transport.d.ts +29 -0
- package/dist/src/transports/memory_transport.d.ts.map +1 -0
- package/dist/src/transports/memory_transport.js +91 -0
- package/dist/src/transports/memory_transport.js.map +1 -0
- package/dist/src/transports/redis_transport.d.ts +50 -0
- package/dist/src/transports/redis_transport.d.ts.map +1 -0
- package/dist/src/transports/redis_transport.js +107 -0
- package/dist/src/transports/redis_transport.js.map +1 -0
- package/dist/src/transports/transport.d.ts +62 -0
- package/dist/src/transports/transport.d.ts.map +1 -0
- package/dist/src/transports/transport.js +2 -0
- package/dist/src/transports/transport.js.map +1 -0
- package/dist/src/types.d.ts +30 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/usage.d.ts +18 -0
- package/dist/src/usage.d.ts.map +1 -0
- package/dist/src/usage.js +52 -0
- package/dist/src/usage.js.map +1 -0
- package/dist/stubs/config/flags.stub +50 -0
- package/dist/stubs/config/flags_dashboard.stub +35 -0
- package/dist/stubs/database/migrations/create_flags_tables.stub +27 -0
- package/dist/stubs/main.d.ts +3 -0
- package/dist/stubs/main.d.ts.map +1 -0
- package/dist/stubs/main.js +9 -0
- package/dist/stubs/main.js.map +1 -0
- package/package.json +181 -0
- package/skills/flags-basics/SKILL.md +142 -0
- package/skills/flags-deployment/SKILL.md +125 -0
- package/skills/flags-experiments/SKILL.md +99 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Ruleset } from '@adonis-agora/flags-core';
|
|
2
|
+
import type { RulesetSource, SourceStatus } from '../ruleset_source.js';
|
|
3
|
+
import type { TokenSource } from './token.js';
|
|
4
|
+
export interface RemoteSourceOptions {
|
|
5
|
+
/** The control-plane base URL (`https://flags.example.com/_flags`). */
|
|
6
|
+
url: string;
|
|
7
|
+
token: TokenSource;
|
|
8
|
+
/** The tenant's partition (sent as `x-flags-tenant`; the token decides what it really is). */
|
|
9
|
+
partition: string;
|
|
10
|
+
/**
|
|
11
|
+
* Local file holding the last ruleset received. Read at boot when the control-plane is
|
|
12
|
+
* unreachable, so a deploy during an outage still serves real targeting. Default: none.
|
|
13
|
+
*/
|
|
14
|
+
snapshotPath?: string;
|
|
15
|
+
/** Use server-sent events for push updates. Default `true`; polling always runs as a fallback. */
|
|
16
|
+
stream?: boolean;
|
|
17
|
+
/** Poll interval (ms). Default 30000 with streaming, 5000 without. */
|
|
18
|
+
intervalMs?: number;
|
|
19
|
+
/** Advertised capabilities (sent as `x-flags-capabilities`). */
|
|
20
|
+
capabilities?: string[];
|
|
21
|
+
fetch?: typeof fetch;
|
|
22
|
+
onError?: (error: unknown) => void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A tenant's ruleset: fetched from the control-plane (conditional GET by ETag), kept fresh by an SSE
|
|
26
|
+
* stream with polling as a fallback, and snapshotted to disk. Evaluation stays local.
|
|
27
|
+
*/
|
|
28
|
+
export declare class RemoteSource implements RulesetSource {
|
|
29
|
+
#private;
|
|
30
|
+
private readonly options;
|
|
31
|
+
constructor(options: RemoteSourceOptions);
|
|
32
|
+
start(): Promise<void>;
|
|
33
|
+
current(): Ruleset;
|
|
34
|
+
onChange(listener: (ruleset: Ruleset) => void): () => void;
|
|
35
|
+
refresh(): Promise<boolean>;
|
|
36
|
+
status(): SourceStatus;
|
|
37
|
+
stop(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=remote_source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote_source.d.ts","sourceRoot":"","sources":["../../../src/topology/remote_source.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwC,KAAK,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;IACnB,8FAA8F;IAC9F,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kGAAkG;IAClG,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED;;;GAGG;AACH,qBAAa,YAAa,YAAW,aAAa;;IAYpC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,mBAAmB,EAExD;IAgBK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAkB3B;IAED,OAAO,IAAI,OAAO,CAEjB;IAED,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAGzD;IAED,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAK1B;IA4FD,MAAM,IAAI,YAAY,CAQrB;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAI1B;CACF"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { emptyRuleset, RULESET_SCHEMA_VERSION } from '@adonis-agora/flags-core';
|
|
4
|
+
/**
|
|
5
|
+
* A tenant's ruleset: fetched from the control-plane (conditional GET by ETag), kept fresh by an SSE
|
|
6
|
+
* stream with polling as a fallback, and snapshotted to disk. Evaluation stays local.
|
|
7
|
+
*/
|
|
8
|
+
export class RemoteSource {
|
|
9
|
+
options;
|
|
10
|
+
#ruleset = emptyRuleset('0');
|
|
11
|
+
#loadedAt = null;
|
|
12
|
+
#lastError = null;
|
|
13
|
+
#listeners = new Set();
|
|
14
|
+
#timer;
|
|
15
|
+
#stream;
|
|
16
|
+
#streaming = false;
|
|
17
|
+
#stopped = false;
|
|
18
|
+
#refreshing;
|
|
19
|
+
#fetch;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.#fetch = options.fetch ?? fetch;
|
|
23
|
+
}
|
|
24
|
+
get #base() {
|
|
25
|
+
return this.options.url.replace(/\/$/, '');
|
|
26
|
+
}
|
|
27
|
+
async #headers() {
|
|
28
|
+
return {
|
|
29
|
+
authorization: `Bearer ${await this.options.token.get()}`,
|
|
30
|
+
'x-flags-tenant': this.options.partition,
|
|
31
|
+
'x-flags-capabilities': (this.options.capabilities ?? [`ruleset:v${RULESET_SCHEMA_VERSION}`]).join(','),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async start() {
|
|
35
|
+
this.#stopped = false;
|
|
36
|
+
const loaded = await this.refresh();
|
|
37
|
+
if (!loaded && !this.#loadedAt && this.options.snapshotPath) {
|
|
38
|
+
try {
|
|
39
|
+
const snapshot = JSON.parse(await readFile(this.options.snapshotPath, 'utf8'));
|
|
40
|
+
this.#set(snapshot, false);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// No snapshot yet: serve code defaults until the control-plane answers.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const streaming = this.options.stream !== false;
|
|
47
|
+
this.#timer = setInterval(() => void this.refresh(), this.options.intervalMs ?? (streaming ? 30_000 : 5000));
|
|
48
|
+
this.#timer.unref();
|
|
49
|
+
if (streaming)
|
|
50
|
+
void this.#openStream();
|
|
51
|
+
}
|
|
52
|
+
current() {
|
|
53
|
+
return this.#ruleset;
|
|
54
|
+
}
|
|
55
|
+
onChange(listener) {
|
|
56
|
+
this.#listeners.add(listener);
|
|
57
|
+
return () => this.#listeners.delete(listener);
|
|
58
|
+
}
|
|
59
|
+
refresh() {
|
|
60
|
+
this.#refreshing ??= this.#refresh().finally(() => {
|
|
61
|
+
this.#refreshing = undefined;
|
|
62
|
+
});
|
|
63
|
+
return this.#refreshing;
|
|
64
|
+
}
|
|
65
|
+
async #refresh() {
|
|
66
|
+
try {
|
|
67
|
+
const headers = await this.#headers();
|
|
68
|
+
if (this.#loadedAt)
|
|
69
|
+
headers['if-none-match'] = `"${this.#ruleset.version}"`;
|
|
70
|
+
const response = await this.#fetch(`${this.#base}/v1/ruleset`, { headers });
|
|
71
|
+
if (response.status === 304) {
|
|
72
|
+
this.#lastError = null;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (response.status === 401)
|
|
76
|
+
this.options.token.invalidate?.();
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
throw new Error(`control-plane answered ${response.status}`);
|
|
79
|
+
const ruleset = (await response.json());
|
|
80
|
+
if (this.#loadedAt && ruleset.version === this.#ruleset.version)
|
|
81
|
+
return false;
|
|
82
|
+
this.#set(ruleset, true);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.#lastError = error instanceof Error ? error.message : String(error);
|
|
87
|
+
this.options.onError?.(error);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
#set(ruleset, persist) {
|
|
92
|
+
this.#ruleset = ruleset;
|
|
93
|
+
this.#loadedAt = new Date();
|
|
94
|
+
if (persist)
|
|
95
|
+
this.#lastError = null;
|
|
96
|
+
for (const listener of this.#listeners) {
|
|
97
|
+
try {
|
|
98
|
+
listener(ruleset);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// ignore
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (persist && this.options.snapshotPath)
|
|
105
|
+
void this.#snapshot(ruleset);
|
|
106
|
+
}
|
|
107
|
+
async #snapshot(ruleset) {
|
|
108
|
+
const path = this.options.snapshotPath;
|
|
109
|
+
try {
|
|
110
|
+
await mkdir(dirname(path), { recursive: true });
|
|
111
|
+
// Write-then-rename: a crash mid-write never leaves a truncated snapshot.
|
|
112
|
+
await writeFile(`${path}.tmp`, JSON.stringify(ruleset));
|
|
113
|
+
await rename(`${path}.tmp`, path);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
this.options.onError?.(error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Reads `event: ruleset` messages; reconnects with backoff while the source runs. */
|
|
120
|
+
async #openStream() {
|
|
121
|
+
let backoff = 1000;
|
|
122
|
+
while (!this.#stopped) {
|
|
123
|
+
this.#stream = new AbortController();
|
|
124
|
+
try {
|
|
125
|
+
const response = await this.#fetch(`${this.#base}/v1/ruleset/stream`, {
|
|
126
|
+
headers: { ...(await this.#headers()), accept: 'text/event-stream' },
|
|
127
|
+
signal: this.#stream.signal,
|
|
128
|
+
});
|
|
129
|
+
if (response.status === 401)
|
|
130
|
+
this.options.token.invalidate?.();
|
|
131
|
+
if (!response.ok || !response.body)
|
|
132
|
+
throw new Error(`stream answered ${response.status}`);
|
|
133
|
+
this.#streaming = true;
|
|
134
|
+
backoff = 1000;
|
|
135
|
+
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
136
|
+
let buffer = '';
|
|
137
|
+
for (;;) {
|
|
138
|
+
const { value, done } = await reader.read();
|
|
139
|
+
if (done)
|
|
140
|
+
break;
|
|
141
|
+
buffer += value;
|
|
142
|
+
let boundary = buffer.indexOf('\n\n');
|
|
143
|
+
while (boundary !== -1) {
|
|
144
|
+
const message = buffer.slice(0, boundary);
|
|
145
|
+
buffer = buffer.slice(boundary + 2);
|
|
146
|
+
if (/^event: ruleset$/m.test(message))
|
|
147
|
+
void this.refresh();
|
|
148
|
+
boundary = buffer.indexOf('\n\n');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (this.#stopped)
|
|
154
|
+
break;
|
|
155
|
+
this.options.onError?.(error);
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
this.#streaming = false;
|
|
159
|
+
}
|
|
160
|
+
if (this.#stopped)
|
|
161
|
+
break;
|
|
162
|
+
// The server closes the stream when the token expires; reconnect fetches a fresh one.
|
|
163
|
+
await new Promise((resolve) => setTimeout(resolve, backoff).unref());
|
|
164
|
+
backoff = Math.min(backoff * 2, 30_000);
|
|
165
|
+
void this.refresh();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
status() {
|
|
169
|
+
return {
|
|
170
|
+
version: this.#ruleset.version,
|
|
171
|
+
loadedAt: this.#loadedAt,
|
|
172
|
+
lastError: this.#lastError,
|
|
173
|
+
stale: this.#lastError !== null && this.#loadedAt !== null,
|
|
174
|
+
mode: this.#streaming ? 'stream' : 'poll',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async stop() {
|
|
178
|
+
this.#stopped = true;
|
|
179
|
+
if (this.#timer)
|
|
180
|
+
clearInterval(this.#timer);
|
|
181
|
+
this.#stream?.abort();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=remote_source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote_source.js","sourceRoot":"","sources":["../../../src/topology/remote_source.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Where a tenant gets the bearer token it presents to the control-plane. */
|
|
2
|
+
export interface TokenSource {
|
|
3
|
+
get(): Promise<string>;
|
|
4
|
+
/** Drops a cached token (after a 401), so the next `get` fetches a fresh one. */
|
|
5
|
+
invalidate?(): void;
|
|
6
|
+
}
|
|
7
|
+
export interface ClientCredentialsOptions {
|
|
8
|
+
/** OAuth token endpoint (authkit-server: `<issuer>/oauth/token`). */
|
|
9
|
+
tokenUrl: string;
|
|
10
|
+
clientId: string;
|
|
11
|
+
clientSecret: string;
|
|
12
|
+
/** Resource the token is for. Default `flags`. */
|
|
13
|
+
audience?: string;
|
|
14
|
+
/** Default: read rules, write events and declarations. */
|
|
15
|
+
scope?: string;
|
|
16
|
+
/** Refresh this long before expiry (ms). Default 30000. */
|
|
17
|
+
leewayMs?: number;
|
|
18
|
+
fetch?: typeof fetch;
|
|
19
|
+
}
|
|
20
|
+
export declare const TENANT_SCOPES: {
|
|
21
|
+
readonly rulesetRead: 'flags:ruleset:read';
|
|
22
|
+
readonly eventsWrite: 'flags:events:write';
|
|
23
|
+
readonly declarationsWrite: 'flags:declarations:write';
|
|
24
|
+
readonly ofrepEvaluate: 'flags:ofrep:evaluate';
|
|
25
|
+
};
|
|
26
|
+
export declare const tokens: {
|
|
27
|
+
/** A fixed token (a shared secret). */
|
|
28
|
+
static(token: string): TokenSource;
|
|
29
|
+
/**
|
|
30
|
+
* OAuth 2.0 client credentials (RFC 6749 §4.4) — with authkit-server, register the app as a
|
|
31
|
+
* confidential client with the `client_credentials` grant. Tokens are cached until shortly before
|
|
32
|
+
* they expire; concurrent callers share one in-flight request.
|
|
33
|
+
*/
|
|
34
|
+
clientCredentials(options: ClientCredentialsOptions): TokenSource;
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../../src/topology/token.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACvB,iFAAiF;IACjF,UAAU,CAAC,IAAI,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,eAAO,MAAM,aAAa;aACxB,WAAW,EAAE,oBAAoB;aACjC,WAAW,EAAE,oBAAoB;aACjC,iBAAiB,EAAE,0BAA0B;aAC7C,aAAa,EAAE,sBAAsB;CAC7B,CAAC;AAEX,eAAO,MAAM,MAAM;IACjB,uCAAuC;IACvC,MAAM,QAAQ,MAAM,GAAG,WAAW;IAIlC;;;;OAIG;IACH,iBAAiB,UAAU,wBAAwB,GAAG,WAAW;CAiDlE,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const TENANT_SCOPES = {
|
|
2
|
+
rulesetRead: 'flags:ruleset:read',
|
|
3
|
+
eventsWrite: 'flags:events:write',
|
|
4
|
+
declarationsWrite: 'flags:declarations:write',
|
|
5
|
+
ofrepEvaluate: 'flags:ofrep:evaluate',
|
|
6
|
+
};
|
|
7
|
+
export const tokens = {
|
|
8
|
+
/** A fixed token (a shared secret). */
|
|
9
|
+
static(token) {
|
|
10
|
+
return { get: async () => token };
|
|
11
|
+
},
|
|
12
|
+
/**
|
|
13
|
+
* OAuth 2.0 client credentials (RFC 6749 §4.4) — with authkit-server, register the app as a
|
|
14
|
+
* confidential client with the `client_credentials` grant. Tokens are cached until shortly before
|
|
15
|
+
* they expire; concurrent callers share one in-flight request.
|
|
16
|
+
*/
|
|
17
|
+
clientCredentials(options) {
|
|
18
|
+
const fetcher = options.fetch ?? fetch;
|
|
19
|
+
let cached;
|
|
20
|
+
let inflight;
|
|
21
|
+
const leeway = options.leewayMs ?? 30_000;
|
|
22
|
+
const fetchToken = async () => {
|
|
23
|
+
const body = new URLSearchParams({
|
|
24
|
+
grant_type: 'client_credentials',
|
|
25
|
+
audience: options.audience ?? 'flags',
|
|
26
|
+
resource: options.audience ?? 'flags',
|
|
27
|
+
scope: options.scope ??
|
|
28
|
+
[
|
|
29
|
+
TENANT_SCOPES.rulesetRead,
|
|
30
|
+
TENANT_SCOPES.eventsWrite,
|
|
31
|
+
TENANT_SCOPES.declarationsWrite,
|
|
32
|
+
].join(' '),
|
|
33
|
+
});
|
|
34
|
+
const response = await fetcher(options.tokenUrl, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
38
|
+
authorization: `Basic ${Buffer.from(`${encodeURIComponent(options.clientId)}:${encodeURIComponent(options.clientSecret)}`).toString('base64')}`,
|
|
39
|
+
},
|
|
40
|
+
body,
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok)
|
|
43
|
+
throw new Error(`token endpoint answered ${response.status}: ${await response.text()}`);
|
|
44
|
+
const json = (await response.json());
|
|
45
|
+
if (!json.access_token)
|
|
46
|
+
throw new Error('token endpoint returned no access_token');
|
|
47
|
+
cached = {
|
|
48
|
+
token: json.access_token,
|
|
49
|
+
expiresAt: Date.now() + (json.expires_in ?? 300) * 1000,
|
|
50
|
+
};
|
|
51
|
+
return json.access_token;
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
async get() {
|
|
55
|
+
if (cached && cached.expiresAt - leeway > Date.now())
|
|
56
|
+
return cached.token;
|
|
57
|
+
inflight ??= fetchToken().finally(() => {
|
|
58
|
+
inflight = undefined;
|
|
59
|
+
});
|
|
60
|
+
return inflight;
|
|
61
|
+
},
|
|
62
|
+
invalidate() {
|
|
63
|
+
cached = undefined;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../../src/topology/token.ts"],"names":[],"mappings":"AAqBA,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,WAAW,EAAE,oBAAoB;IACjC,WAAW,EAAE,oBAAoB;IACjC,iBAAiB,EAAE,0BAA0B;IAC7C,aAAa,EAAE,sBAAsB;CAC7B,CAAC;AAEX,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,uCAAuC;IACvC,MAAM,CAAC,KAAa;QAClB,OAAO,EAAE,GAAG,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;IACpC,CAAC;IAED;;;;OAIG;IACH,iBAAiB,CAAC,OAAiC;QACjD,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC;QACvC,IAAI,MAAwD,CAAC;QAC7D,IAAI,QAAqC,CAAC;QAC1C,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC;QAC1C,MAAM,UAAU,GAAG,KAAK,IAAI,EAAE;YAC5B,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;gBAC/B,UAAU,EAAE,oBAAoB;gBAChC,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,OAAO;gBACrC,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,OAAO;gBACrC,KAAK,EACH,OAAO,CAAC,KAAK;oBACb;wBACE,aAAa,CAAC,WAAW;wBACzB,aAAa,CAAC,WAAW;wBACzB,aAAa,CAAC,iBAAiB;qBAChC,CAAC,IAAI,CAAC,GAAG,CAAC;aACd,CAAC,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE;gBAC/C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,mCAAmC;oBACnD,aAAa,EAAE,SAAS,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,kBAAkB,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;iBAChJ;gBACD,IAAI;aACL,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE;gBACd,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,CAAC,MAAM,KAAK,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAC1F,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAmD,CAAC;YACvF,IAAI,CAAC,IAAI,CAAC,YAAY;gBAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;YACnF,MAAM,GAAG;gBACP,KAAK,EAAE,IAAI,CAAC,YAAY;gBACxB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,GAAG,IAAI;aACxD,CAAC;YACF,OAAO,IAAI,CAAC,YAAY,CAAC;QAC3B,CAAC,CAAC;QACF,OAAO;YACL,KAAK,CAAC,GAAG;gBACP,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,GAAG,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE;oBAAE,OAAO,MAAM,CAAC,KAAK,CAAC;gBAC1E,QAAQ,KAAK,UAAU,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;oBACrC,QAAQ,GAAG,SAAS,CAAC;gBACvB,CAAC,CAAC,CAAC;gBACH,OAAO,QAAQ,CAAC;YAClB,CAAC;YACD,UAAU;gBACR,MAAM,GAAG,SAAS,CAAC;YACrB,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Subject } from '../subject.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a control-plane trusts about a request: derived from the token, never from the body. Same
|
|
4
|
+
* shape as `@adonis-agora/durable`'s `VerifiedTenant`, so one verifier can serve both.
|
|
5
|
+
*/
|
|
6
|
+
export interface VerifiedTenant {
|
|
7
|
+
/** The tenant's partition (its app name). */
|
|
8
|
+
tenant: string;
|
|
9
|
+
/** Scopes granted (see `TENANT_SCOPES`). */
|
|
10
|
+
capabilities?: string[] | undefined;
|
|
11
|
+
/** The environment whose ruleset this tenant reads. Default: the control-plane's. */
|
|
12
|
+
environment?: string;
|
|
13
|
+
/** Epoch ms after which streams opened with this token must close. */
|
|
14
|
+
expiresAt?: number;
|
|
15
|
+
}
|
|
16
|
+
export type TenantVerifier = (input: {
|
|
17
|
+
token?: string | undefined;
|
|
18
|
+
tenant?: string | undefined;
|
|
19
|
+
}) => VerifiedTenant | null | Promise<VerifiedTenant | null>;
|
|
20
|
+
export interface SharedSecretTenant {
|
|
21
|
+
secret: string;
|
|
22
|
+
environment?: string;
|
|
23
|
+
/** Default: every tenant scope. */
|
|
24
|
+
capabilities?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface AuthkitVerifierOptions {
|
|
27
|
+
/** The authkit-server issuer (the `iss` claim). */
|
|
28
|
+
issuer: string;
|
|
29
|
+
/** Its JWKS URI. Default `<issuer>/.well-known/jwks.json`. */
|
|
30
|
+
jwksUri?: string;
|
|
31
|
+
/** Expected `aud`. Default `flags`. */
|
|
32
|
+
audience?: string | string[];
|
|
33
|
+
/** Claim holding the partition. Default `client_id`. */
|
|
34
|
+
partitionClaim?: string;
|
|
35
|
+
/** Partition → environment, when tenants read different environments. */
|
|
36
|
+
environments?: Record<string, string>;
|
|
37
|
+
/** Accept tokens without RFC 9068's `typ: at+jwt`. Default false. */
|
|
38
|
+
allowAnyTyp?: boolean;
|
|
39
|
+
/** @internal Also return the verified claims (used by the browser verifier). */
|
|
40
|
+
claims?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare const verifiers: {
|
|
43
|
+
/**
|
|
44
|
+
* One secret per tenant, compared in constant time. The request names its partition with the
|
|
45
|
+
* `x-flags-tenant` header; the secret proves it.
|
|
46
|
+
*/
|
|
47
|
+
sharedSecret(tenants: Record<string, string | SharedSecretTenant>): TenantVerifier;
|
|
48
|
+
/**
|
|
49
|
+
* JWT access tokens issued by authkit-server (`client_credentials`), verified locally against its
|
|
50
|
+
* JWKS through `@adonis-agora/authkit-client` (optional peer). The partition comes from the
|
|
51
|
+
* token's `client_id` claim and the capabilities from its `scope`.
|
|
52
|
+
*/
|
|
53
|
+
authkit(options: AuthkitVerifierOptions): TenantVerifier;
|
|
54
|
+
};
|
|
55
|
+
/** A browser's verified identity: who is evaluating, from a token the browser cannot forge. */
|
|
56
|
+
export interface VerifiedUser {
|
|
57
|
+
/** Absent for anonymous visitors: they get static values only (no targeting key). */
|
|
58
|
+
subject?: Subject;
|
|
59
|
+
/** The environment whose ruleset applies. Default: the control-plane's. */
|
|
60
|
+
environment?: string;
|
|
61
|
+
/** The app the browser belongs to (stamped on its events). Default `browser`. */
|
|
62
|
+
app?: string;
|
|
63
|
+
/** Epoch ms after which streams opened with this token close. */
|
|
64
|
+
expiresAt?: number;
|
|
65
|
+
}
|
|
66
|
+
export type UserVerifier = (input: {
|
|
67
|
+
token?: string | undefined;
|
|
68
|
+
}) => VerifiedUser | null | Promise<VerifiedUser | null>;
|
|
69
|
+
export interface AuthkitUserVerifierOptions {
|
|
70
|
+
issuer: string;
|
|
71
|
+
jwksUri?: string;
|
|
72
|
+
/** Expected `aud` of the user's access token (your API / the flags server). */
|
|
73
|
+
audience: string | string[];
|
|
74
|
+
/** Partition → environment, keyed by the token's `client_id` (the app the user signed into). */
|
|
75
|
+
environments?: Record<string, string>;
|
|
76
|
+
allowAnyTyp?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export declare const userVerifiers: {
|
|
79
|
+
/**
|
|
80
|
+
* An authkit access token held by the client itself — a mobile app or SPA that signs in with
|
|
81
|
+
* PKCE — verified against the issuer's JWKS. Browsers of an Adonis app with a session use the
|
|
82
|
+
* app's token endpoint instead (`browser` in `config/flags.ts`). The
|
|
83
|
+
* subject is the `sub` claim — the same person in every app — and `email`, `roles`/`global_roles`
|
|
84
|
+
* and the `org_*` claims become trusted targeting attributes.
|
|
85
|
+
*/
|
|
86
|
+
authkit(options: AuthkitUserVerifierOptions): UserVerifier;
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/topology/verify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG7C;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACpC,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE;IACnC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B,KAAK,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mCAAmC;IACnC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gFAAgF;IAChF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAUD,eAAO,MAAM,SAAS;IACpB;;;OAGG;IACH,YAAY,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAAC,GAAG,cAAc;IAelF;;;;OAIG;IACH,OAAO,UAAU,sBAAsB,GAAG,cAAc;CA4CzD,CAAC;AAEF,+FAA+F;AAC/F,MAAM,WAAW,YAAY;IAC3B,qFAAqF;IACrF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B,KAAK,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;AAEzD,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,eAAO,MAAM,aAAa;IACxB;;;;;;OAMG;IACH,OAAO,UAAU,0BAA0B,GAAG,YAAY;CAwC3D,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { TENANT_SCOPES } from './token.js';
|
|
3
|
+
const ALL_SCOPES = Object.values(TENANT_SCOPES);
|
|
4
|
+
function sameSecret(a, b) {
|
|
5
|
+
const x = Buffer.from(a);
|
|
6
|
+
const y = Buffer.from(b);
|
|
7
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
8
|
+
}
|
|
9
|
+
export const verifiers = {
|
|
10
|
+
/**
|
|
11
|
+
* One secret per tenant, compared in constant time. The request names its partition with the
|
|
12
|
+
* `x-flags-tenant` header; the secret proves it.
|
|
13
|
+
*/
|
|
14
|
+
sharedSecret(tenants) {
|
|
15
|
+
return ({ token, tenant }) => {
|
|
16
|
+
if (!token || !tenant)
|
|
17
|
+
return null;
|
|
18
|
+
const entry = tenants[tenant];
|
|
19
|
+
if (entry === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
const config = typeof entry === 'string' ? { secret: entry } : entry;
|
|
22
|
+
if (!sameSecret(token, config.secret))
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
tenant,
|
|
26
|
+
capabilities: config.capabilities ?? [...ALL_SCOPES],
|
|
27
|
+
...(config.environment ? { environment: config.environment } : {}),
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
/**
|
|
32
|
+
* JWT access tokens issued by authkit-server (`client_credentials`), verified locally against its
|
|
33
|
+
* JWKS through `@adonis-agora/authkit-client` (optional peer). The partition comes from the
|
|
34
|
+
* token's `client_id` claim and the capabilities from its `scope`.
|
|
35
|
+
*/
|
|
36
|
+
authkit(options) {
|
|
37
|
+
const issuer = options.issuer.replace(/\/$/, '');
|
|
38
|
+
let verify;
|
|
39
|
+
return async ({ token }) => {
|
|
40
|
+
if (!token)
|
|
41
|
+
return null;
|
|
42
|
+
if (!verify) {
|
|
43
|
+
try {
|
|
44
|
+
const mod = (await import('@adonis-agora/authkit-client'));
|
|
45
|
+
verify = mod.verifyJwtAccessToken;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error('@adonis-agora/flags: verifiers.authkit() needs "@adonis-agora/authkit-client". Install it, or use verifiers.sharedSecret().');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let claims;
|
|
52
|
+
try {
|
|
53
|
+
claims = await verify(token, {
|
|
54
|
+
issuer: options.issuer,
|
|
55
|
+
jwksUri: options.jwksUri ?? `${issuer}/.well-known/jwks.json`,
|
|
56
|
+
audience: options.audience ?? 'flags',
|
|
57
|
+
...(options.allowAnyTyp ? { allowAnyTyp: true } : {}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const partition = claims[options.partitionClaim ?? 'client_id'];
|
|
64
|
+
if (typeof partition !== 'string' || partition.length === 0)
|
|
65
|
+
return null;
|
|
66
|
+
const scope = typeof claims.scope === 'string' ? claims.scope.split(/\s+/).filter(Boolean) : [];
|
|
67
|
+
const environment = options.environments?.[partition];
|
|
68
|
+
return {
|
|
69
|
+
tenant: partition,
|
|
70
|
+
...(options.claims ? { claims } : {}),
|
|
71
|
+
capabilities: scope,
|
|
72
|
+
...(environment ? { environment } : {}),
|
|
73
|
+
...(typeof claims.exp === 'number' ? { expiresAt: claims.exp * 1000 } : {}),
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
export const userVerifiers = {
|
|
79
|
+
/**
|
|
80
|
+
* An authkit access token held by the client itself — a mobile app or SPA that signs in with
|
|
81
|
+
* PKCE — verified against the issuer's JWKS. Browsers of an Adonis app with a session use the
|
|
82
|
+
* app's token endpoint instead (`browser` in `config/flags.ts`). The
|
|
83
|
+
* subject is the `sub` claim — the same person in every app — and `email`, `roles`/`global_roles`
|
|
84
|
+
* and the `org_*` claims become trusted targeting attributes.
|
|
85
|
+
*/
|
|
86
|
+
authkit(options) {
|
|
87
|
+
const tenant = verifiers.authkit({
|
|
88
|
+
issuer: options.issuer,
|
|
89
|
+
audience: options.audience,
|
|
90
|
+
partitionClaim: 'sub',
|
|
91
|
+
...(options.jwksUri ? { jwksUri: options.jwksUri } : {}),
|
|
92
|
+
...(options.allowAnyTyp ? { allowAnyTyp: true } : {}),
|
|
93
|
+
claims: true,
|
|
94
|
+
});
|
|
95
|
+
return async ({ token }) => {
|
|
96
|
+
const verified = (await tenant({ token }));
|
|
97
|
+
if (!verified?.claims)
|
|
98
|
+
return null;
|
|
99
|
+
const claims = verified.claims;
|
|
100
|
+
const attributes = {};
|
|
101
|
+
const email = typeof claims.email === 'string' ? claims.email : undefined;
|
|
102
|
+
if (email) {
|
|
103
|
+
attributes.email = email;
|
|
104
|
+
attributes.emailDomain = email.split('@')[1]?.toLowerCase();
|
|
105
|
+
}
|
|
106
|
+
const roles = claims.global_roles ?? claims.roles;
|
|
107
|
+
if (Array.isArray(roles))
|
|
108
|
+
attributes.globalRoles = roles.map(String);
|
|
109
|
+
for (const [claim, attribute] of [
|
|
110
|
+
['org_id', 'orgId'],
|
|
111
|
+
['org_slug', 'orgSlug'],
|
|
112
|
+
['org_role', 'orgRole'],
|
|
113
|
+
]) {
|
|
114
|
+
if (typeof claims[claim] === 'string')
|
|
115
|
+
attributes[attribute] = claims[claim];
|
|
116
|
+
}
|
|
117
|
+
const clientId = typeof claims.client_id === 'string' ? claims.client_id : undefined;
|
|
118
|
+
const environment = clientId ? options.environments?.[clientId] : undefined;
|
|
119
|
+
return {
|
|
120
|
+
subject: { id: verified.tenant, type: 'user', attributes },
|
|
121
|
+
...(environment ? { environment } : {}),
|
|
122
|
+
...(clientId ? { app: clientId } : {}),
|
|
123
|
+
...(verified.expiresAt ? { expiresAt: verified.expiresAt } : {}),
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/topology/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AA8C3C,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;AAEhD,SAAS,UAAU,CAAC,CAAS,EAAE,CAAS;IACtC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzB,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzB,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB;;;OAGG;IACH,YAAY,CAAC,OAAoD;QAC/D,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;YAC3B,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAC;YACnC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;YAC9B,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAC;YACrC,MAAM,MAAM,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;YACrE,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC;gBAAE,OAAO,IAAI,CAAC;YACnD,OAAO;gBACL,MAAM;gBACN,YAAY,EAAE,MAAM,CAAC,YAAY,IAAI,CAAC,GAAG,UAAU,CAAC;gBACpD,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACnE,CAAC;QACJ,CAAC,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,OAA+B;QACrC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACjD,IAAI,MAES,CAAC;QACd,OAAO,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;YACzB,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAC;YACxB,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,8BAA8B,CAAC,CAExD,CAAC;oBACF,MAAM,GAAG,GAAG,CAAC,oBAAoB,CAAC;gBACpC,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,IAAI,KAAK,CACb,6HAA6H,CAC9H,CAAC;gBACJ,CAAC;YACH,CAAC;YACD,IAAI,MAA+B,CAAC;YACpC,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,MAAO,CAAC,KAAK,EAAE;oBAC5B,MAAM,EAAE,OAAO,CAAC,MAAM;oBACtB,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,GAAG,MAAM,wBAAwB;oBAC7D,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,OAAO;oBACrC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACtD,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,cAAc,IAAI,WAAW,CAAC,CAAC;YAChE,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACzE,MAAM,KAAK,GACT,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACpF,MAAM,WAAW,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,SAAS,CAAC,CAAC;YACtD,OAAO;gBACL,MAAM,EAAE,SAAS;gBACjB,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrC,YAAY,EAAE,KAAK;gBACnB,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvC,GAAG,CAAC,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5E,CAAC;QACJ,CAAC,CAAC;IACJ,CAAC;CACF,CAAC;AA4BF,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B;;;;;;OAMG;IACH,OAAO,CAAC,OAAmC;QACzC,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC;YAC/B,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,cAAc,EAAE,KAAK;YACrB,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxD,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,MAAM,EAAE,IAAI;SACa,CAAC,CAAC;QAC7B,OAAO,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;YACzB,MAAM,QAAQ,GAAG,CAAC,MAAM,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAEjC,CAAC;YACT,IAAI,CAAC,QAAQ,EAAE,MAAM;gBAAE,OAAO,IAAI,CAAC;YACnC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;YAC/B,MAAM,UAAU,GAA4B,EAAE,CAAC;YAC/C,MAAM,KAAK,GAAG,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;YAC1E,IAAI,KAAK,EAAE,CAAC;gBACV,UAAU,CAAC,KAAK,GAAG,KAAK,CAAC;gBACzB,UAAU,CAAC,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;YAC9D,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,KAAK,CAAC;YAClD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBAAE,UAAU,CAAC,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACrE,KAAK,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI;gBAC/B,CAAC,QAAQ,EAAE,OAAO,CAAC;gBACnB,CAAC,UAAU,EAAE,SAAS,CAAC;gBACvB,CAAC,UAAU,EAAE,SAAS,CAAC;aACf,EAAE,CAAC;gBACX,IAAI,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,QAAQ;oBAAE,UAAU,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;YAC/E,CAAC;YACD,MAAM,QAAQ,GAAG,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;YACrF,MAAM,WAAW,GAAG,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAC5E,OAAO;gBACL,OAAO,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE;gBAC1D,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACtC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACjE,CAAC;QACJ,CAAC,CAAC;IACJ,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type FlagEvent } from '../events/event.js';
|
|
2
|
+
import { type SchemaOptions } from '../schema.js';
|
|
3
|
+
import { type Dialect, type SqlClient, type SqlDatabase } from '../sql.js';
|
|
4
|
+
import type { Delivery, PublishOptions, SinkStatus, Transport } from './transport.js';
|
|
5
|
+
export interface DbTransportOptions extends SchemaOptions {
|
|
6
|
+
/**
|
|
7
|
+
* MySQL only. InnoDB hands out auto-increment ids before commit, so a slow transaction can
|
|
8
|
+
* commit an id lower than one already relayed. The relay only reads events older than this
|
|
9
|
+
* window (ms) to leave such transactions time to commit. Default 10000. Postgres does not need
|
|
10
|
+
* it (it orders by transaction id and reads only finished transactions); SQLite has a single
|
|
11
|
+
* writer.
|
|
12
|
+
*/
|
|
13
|
+
mysqlSafetyMs?: number;
|
|
14
|
+
autoCreateSchema?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The event log in your database (`flags_events`), with one cursor per sink in
|
|
18
|
+
* `flags_sink_cursors`. The only transactional transport: `publish` accepts the caller's Lucid
|
|
19
|
+
* transaction.
|
|
20
|
+
*
|
|
21
|
+
* Leases are `UPDATE … WHERE lease expired` (no transaction held open while a sink sends), and the
|
|
22
|
+
* cursor only moves while the lease is still ours, so a replica that stalled past its lease cannot
|
|
23
|
+
* move it back. Delivery is at-least-once; sinks dedupe by event id.
|
|
24
|
+
*
|
|
25
|
+
* Gap safety: ids are assigned before commit, so reading strictly by id could skip an event whose
|
|
26
|
+
* transaction committed late. On Postgres the cursor is `(xid, id)` and the relay reads only rows of
|
|
27
|
+
* transactions older than the oldest still running (`pg_snapshot_xmin`), which closes the gap
|
|
28
|
+
* exactly. MySQL uses a time window (`mysqlSafetyMs`); SQLite serializes writers.
|
|
29
|
+
*/
|
|
30
|
+
export declare class DbTransport implements Transport {
|
|
31
|
+
#private;
|
|
32
|
+
readonly db: SqlDatabase;
|
|
33
|
+
private readonly options;
|
|
34
|
+
readonly name = "db";
|
|
35
|
+
readonly capabilities: {
|
|
36
|
+
transactional: boolean;
|
|
37
|
+
durable: boolean;
|
|
38
|
+
};
|
|
39
|
+
readonly dialect: Dialect;
|
|
40
|
+
constructor(db: SqlDatabase, options?: DbTransportOptions);
|
|
41
|
+
setup(): Promise<void>;
|
|
42
|
+
publish(events: FlagEvent[], options?: PublishOptions): Promise<void>;
|
|
43
|
+
consume(sink: string, options: {
|
|
44
|
+
batchSize: number;
|
|
45
|
+
leaseMs: number;
|
|
46
|
+
owner: string;
|
|
47
|
+
}): Promise<Delivery | null>;
|
|
48
|
+
rewind(sink: string): Promise<void>;
|
|
49
|
+
status(sinks: string[]): Promise<SinkStatus[]>;
|
|
50
|
+
safePruneId(sinks: string[]): Promise<number>;
|
|
51
|
+
}
|
|
52
|
+
export declare function toEvent(row: Record<string, unknown>): FlagEvent;
|
|
53
|
+
/** Exposed for the db transport's SQL client typing in tests. */
|
|
54
|
+
export type { SqlClient };
|
|
55
|
+
//# sourceMappingURL=db_transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db_transport.d.ts","sourceRoot":"","sources":["../../../src/transports/db_transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAA4C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAC5F,OAAO,EACL,KAAK,OAAO,EAMZ,KAAK,SAAS,EACd,KAAK,WAAW,EAEjB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEtF,MAAM,WAAW,kBAAmB,SAAQ,aAAa;IACvD;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAmBD;;;;;;;;;;;;;GAaG;AACH,qBAAa,WAAY,YAAW,SAAS;;IAMzC,QAAQ,CAAC,EAAE,EAAE,WAAW;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,QAAQ,CAAC,IAAI,QAAQ;IACrB,QAAQ,CAAC,YAAY;QAAK,aAAa;QAAQ,OAAO;MAAS;IAC/D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAE1B,YACW,EAAE,EAAE,WAAW,EACP,OAAO,GAAE,kBAAuB,EAGlD;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAG3B;IAEK,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B9E;IAEK,OAAO,CACX,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAC7D,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAmE1B;IAuCK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxC;IAEK,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CA0BnD;IAEK,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBlD;CACF;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAqB/D;AAED,iEAAiE;AACjE,YAAY,EAAE,SAAS,EAAE,CAAC"}
|