@honua/react 0.0.20-alpha.0 → 0.1.1-beta.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/contract/discovery.d.ts +231 -0
- package/contract/discovery.d.ts.map +1 -0
- package/contract/discovery.js +554 -0
- package/contract/edit-sketch.d.ts +8 -0
- package/contract/edit-sketch.d.ts.map +1 -1
- package/contract/edit-sketch.js +15 -0
- package/contract/edit-snapping.d.ts +160 -0
- package/contract/edit-snapping.d.ts.map +1 -0
- package/contract/edit-snapping.js +675 -0
- package/contract/generated/projjson-v0.7-crs-validator.d.ts +35 -0
- package/contract/generated/projjson-v0.7-crs-validator.d.ts.map +1 -0
- package/contract/generated/projjson-v0.7-crs-validator.js +3941 -0
- package/contract/index.d.ts +12 -1
- package/contract/index.d.ts.map +1 -1
- package/contract/index.js +4 -0
- package/contract/pmtiles.d.ts +193 -0
- package/contract/pmtiles.d.ts.map +1 -0
- package/contract/pmtiles.js +195 -0
- package/contract/protocol-module.d.ts +70 -0
- package/contract/protocol-module.d.ts.map +1 -0
- package/contract/protocol-module.js +1 -0
- package/contract/schema-envelope.d.ts +26 -0
- package/contract/schema-envelope.d.ts.map +1 -0
- package/contract/schema-envelope.js +25 -0
- package/contract/schema.d.ts +582 -0
- package/contract/schema.d.ts.map +1 -0
- package/contract/schema.js +2483 -0
- package/contract/source-capability-support.d.ts +6 -0
- package/contract/source-capability-support.d.ts.map +1 -0
- package/contract/source-capability-support.js +154 -0
- package/contract/source.d.ts +57 -15
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +368 -65
- package/contract/types.d.ts +171 -6
- package/contract/types.d.ts.map +1 -1
- package/contract/types.js +14 -14
- package/core/auth/client-credentials-provider.d.ts +51 -0
- package/core/auth/client-credentials-provider.d.ts.map +1 -0
- package/core/auth/client-credentials-provider.js +115 -0
- package/core/auth/credential-store.d.ts +52 -0
- package/core/auth/credential-store.d.ts.map +1 -0
- package/core/auth/credential-store.js +82 -0
- package/core/auth/index.d.ts +23 -0
- package/core/auth/index.d.ts.map +1 -0
- package/core/auth/index.js +20 -0
- package/core/auth/oauth2-provider.d.ts +114 -0
- package/core/auth/oauth2-provider.d.ts.map +1 -0
- package/core/auth/oauth2-provider.js +408 -0
- package/core/auth/pkce.d.ts +25 -0
- package/core/auth/pkce.d.ts.map +1 -0
- package/core/auth/pkce.js +46 -0
- package/core/auth/static-providers.d.ts +20 -0
- package/core/auth/static-providers.d.ts.map +1 -0
- package/core/auth/static-providers.js +26 -0
- package/core/auth/token-endpoint.d.ts +40 -0
- package/core/auth/token-endpoint.d.ts.map +1 -0
- package/core/auth/token-endpoint.js +133 -0
- package/core/auth/types.d.ts +144 -0
- package/core/auth/types.d.ts.map +1 -0
- package/core/auth/types.js +8 -0
- package/core/bounded-json.d.ts +4 -0
- package/core/bounded-json.d.ts.map +1 -0
- package/core/bounded-json.js +38 -0
- package/core/cache-state.d.ts +9 -0
- package/core/cache-state.d.ts.map +1 -1
- package/core/cache-state.js +10 -0
- package/core/capabilities-xml.d.ts +33 -0
- package/core/capabilities-xml.d.ts.map +1 -0
- package/core/capabilities-xml.js +331 -0
- package/core/client.d.ts +35 -2
- package/core/client.d.ts.map +1 -1
- package/core/client.js +167 -29
- package/core/error-classifications.d.ts +153 -0
- package/core/error-classifications.d.ts.map +1 -0
- package/core/error-classifications.js +140 -0
- package/core/error-code-registry.d.ts +158 -0
- package/core/error-code-registry.d.ts.map +1 -0
- package/core/error-code-registry.js +143 -0
- package/core/error-envelope.d.ts +68 -0
- package/core/error-envelope.d.ts.map +1 -0
- package/core/error-envelope.js +357 -0
- package/core/errors.d.ts +73 -25
- package/core/errors.d.ts.map +1 -1
- package/core/errors.js +113 -46
- package/core/geoparquet-sql.d.ts +190 -0
- package/core/geoparquet-sql.d.ts.map +1 -0
- package/core/geoparquet-sql.js +368 -0
- package/core/odata-key-path.d.ts +12 -0
- package/core/odata-key-path.d.ts.map +1 -0
- package/core/odata-key-path.js +28 -0
- package/core/odata-write-codec.d.ts +66 -0
- package/core/odata-write-codec.d.ts.map +1 -0
- package/core/odata-write-codec.js +1083 -0
- package/core/odata.d.ts +59 -7
- package/core/odata.d.ts.map +1 -1
- package/core/odata.js +431 -99
- package/core/ogc-endpoint-layout.d.ts +70 -0
- package/core/ogc-endpoint-layout.d.ts.map +1 -0
- package/core/ogc-endpoint-layout.js +173 -0
- package/core/ogc-features.d.ts.map +1 -1
- package/core/ogc-features.js +35 -17
- package/core/ogc-maps.d.ts +7 -0
- package/core/ogc-maps.d.ts.map +1 -1
- package/core/ogc-maps.js +46 -7
- package/core/ogc-processes.d.ts +2 -1
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +33 -5
- package/core/ogc-records.d.ts +9 -0
- package/core/ogc-records.d.ts.map +1 -1
- package/core/ogc-records.js +78 -29
- package/core/ogc-tiles.d.ts +10 -1
- package/core/ogc-tiles.d.ts.map +1 -1
- package/core/ogc-tiles.js +56 -17
- package/core/path-utils.d.ts +9 -0
- package/core/path-utils.d.ts.map +1 -1
- package/core/path-utils.js +19 -0
- package/core/protocol-transport.d.ts +1 -0
- package/core/protocol-transport.d.ts.map +1 -1
- package/core/request-pipeline.d.ts.map +1 -1
- package/core/request-pipeline.js +1 -1
- package/core/spatial-filter.d.ts +8 -4
- package/core/spatial-filter.d.ts.map +1 -1
- package/core/spatial-filter.js +206 -16
- package/core/stac-static.d.ts +55 -0
- package/core/stac-static.d.ts.map +1 -0
- package/core/stac-static.js +386 -0
- package/core/stac.d.ts +12 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +45 -15
- package/core/surfaces.d.ts +13 -0
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +127 -16
- package/core/types.d.ts +174 -7
- package/core/types.d.ts.map +1 -1
- package/core/wfs-capabilities.d.ts +12 -0
- package/core/wfs-capabilities.d.ts.map +1 -1
- package/core/wfs-capabilities.js +22 -3
- package/core/wfs.d.ts +13 -0
- package/core/wfs.d.ts.map +1 -1
- package/core/wfs.js +35 -5
- package/core/wms-capabilities.d.ts +56 -84
- package/core/wms-capabilities.d.ts.map +1 -1
- package/core/wms-capabilities.js +212 -509
- package/core/wmts-capabilities.d.ts +62 -62
- package/core/wmts-capabilities.d.ts.map +1 -1
- package/core/wmts-capabilities.js +224 -371
- package/esri-compat/editor.d.ts +8 -0
- package/esri-compat/editor.d.ts.map +1 -1
- package/esri-compat/editor.js +13 -0
- package/esri-compat/feature-layer.d.ts +2 -0
- package/esri-compat/feature-layer.d.ts.map +1 -1
- package/esri-compat/feature-layer.js +1 -0
- package/esri-compat/identity-manager.d.ts +43 -0
- package/esri-compat/identity-manager.d.ts.map +1 -1
- package/esri-compat/identity-manager.js +131 -1
- package/esri-compat/layer-list.d.ts +11 -0
- package/esri-compat/layer-list.d.ts.map +1 -1
- package/esri-compat/layer-list.js +49 -0
- package/esri-compat/legend.d.ts +10 -0
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +27 -0
- package/esri-compat/oauth-info.d.ts +15 -0
- package/esri-compat/oauth-info.d.ts.map +1 -1
- package/esri-compat/oauth-info.js +27 -0
- package/esri-compat/portal.d.ts +197 -0
- package/esri-compat/portal.d.ts.map +1 -0
- package/esri-compat/portal.js +251 -0
- package/esri-compat/renderer-objects.d.ts +40 -0
- package/esri-compat/renderer-objects.d.ts.map +1 -0
- package/esri-compat/renderer-objects.js +62 -0
- package/esri-compat/sketch.d.ts +43 -0
- package/esri-compat/sketch.d.ts.map +1 -1
- package/esri-compat/sketch.js +46 -0
- package/esri-compat/snapping.d.ts +43 -0
- package/esri-compat/snapping.d.ts.map +1 -0
- package/esri-compat/snapping.js +45 -0
- package/esri-compat/url.d.ts.map +1 -1
- package/esri-compat/url.js +1 -1
- package/esri-compat/widget-host.d.ts +84 -0
- package/esri-compat/widget-host.d.ts.map +1 -0
- package/esri-compat/widget-host.js +159 -0
- package/filter-registry/index.d.ts +145 -0
- package/filter-registry/index.d.ts.map +1 -0
- package/filter-registry/index.js +531 -0
- package/generated-app/errors.d.ts +18 -2
- package/generated-app/errors.d.ts.map +1 -1
- package/generated-app/errors.js +44 -11
- package/map/automatic-mount-integration.d.ts +134 -0
- package/map/automatic-mount-integration.d.ts.map +1 -0
- package/map/automatic-mount-integration.js +305 -0
- package/map/automatic-source-strategy.d.ts +125 -0
- package/map/automatic-source-strategy.d.ts.map +1 -0
- package/map/automatic-source-strategy.js +494 -0
- package/map/data-to-map-bridge.d.ts +293 -0
- package/map/data-to-map-bridge.d.ts.map +1 -0
- package/map/data-to-map-bridge.js +817 -0
- package/map/geojson-projection.d.ts +33 -0
- package/map/geojson-projection.d.ts.map +1 -0
- package/map/geojson-projection.js +118 -0
- package/map/index.d.ts +14 -0
- package/map/index.d.ts.map +1 -1
- package/map/index.js +8 -0
- package/map/raster-source-spec.d.ts +30 -0
- package/map/raster-source-spec.d.ts.map +1 -0
- package/map/raster-source-spec.js +190 -0
- package/map/raster-source-strategy.d.ts +65 -0
- package/map/raster-source-strategy.d.ts.map +1 -0
- package/map/raster-source-strategy.js +232 -0
- package/map/source-to-maplibre.d.ts +91 -0
- package/map/source-to-maplibre.d.ts.map +1 -0
- package/map/source-to-maplibre.js +454 -0
- package/map/temporal-playback.d.ts +139 -0
- package/map/temporal-playback.d.ts.map +1 -0
- package/map/temporal-playback.js +247 -0
- package/package.json +19 -1
- package/query-planner/canonical.d.ts +7 -0
- package/query-planner/canonical.d.ts.map +1 -0
- package/query-planner/canonical.js +145 -0
- package/query-planner/cql2-json.d.ts +17 -0
- package/query-planner/cql2-json.d.ts.map +1 -0
- package/query-planner/cql2-json.js +646 -0
- package/query-planner/diagnostics.d.ts +35 -0
- package/query-planner/diagnostics.d.ts.map +1 -0
- package/query-planner/diagnostics.js +373 -0
- package/query-planner/duckdb.d.ts +79 -0
- package/query-planner/duckdb.d.ts.map +1 -0
- package/query-planner/duckdb.js +758 -0
- package/query-planner/execution-context.d.ts +5 -0
- package/query-planner/execution-context.d.ts.map +1 -0
- package/query-planner/execution-context.js +50 -0
- package/query-planner/executor.d.ts +5 -0
- package/query-planner/executor.d.ts.map +1 -0
- package/query-planner/executor.js +223 -0
- package/query-planner/geoservices.d.ts +91 -0
- package/query-planner/geoservices.d.ts.map +1 -0
- package/query-planner/geoservices.js +667 -0
- package/query-planner/grpc.d.ts +100 -0
- package/query-planner/grpc.d.ts.map +1 -0
- package/query-planner/grpc.js +736 -0
- package/query-planner/index.d.ts +44 -0
- package/query-planner/index.d.ts.map +1 -0
- package/query-planner/index.js +30 -0
- package/query-planner/ir.d.ts +20 -0
- package/query-planner/ir.d.ts.map +1 -0
- package/query-planner/ir.js +417 -0
- package/query-planner/local-aggregate.d.ts +5 -0
- package/query-planner/local-aggregate.d.ts.map +1 -0
- package/query-planner/local-aggregate.js +126 -0
- package/query-planner/odata.d.ts +48 -0
- package/query-planner/odata.d.ts.map +1 -0
- package/query-planner/odata.js +662 -0
- package/query-planner/ogc-compiler.d.ts +55 -0
- package/query-planner/ogc-compiler.d.ts.map +1 -0
- package/query-planner/ogc-compiler.js +480 -0
- package/query-planner/ogc-features.d.ts +53 -0
- package/query-planner/ogc-features.d.ts.map +1 -0
- package/query-planner/ogc-features.js +617 -0
- package/query-planner/planner.d.ts +17 -0
- package/query-planner/planner.d.ts.map +1 -0
- package/query-planner/planner.js +818 -0
- package/query-planner/resource.d.ts +94 -0
- package/query-planner/resource.d.ts.map +1 -0
- package/query-planner/resource.js +526 -0
- package/query-planner/semantic-canonical.d.ts +15 -0
- package/query-planner/semantic-canonical.d.ts.map +1 -0
- package/query-planner/semantic-canonical.js +62 -0
- package/query-planner/semantic-compat.d.ts +10 -0
- package/query-planner/semantic-compat.d.ts.map +1 -0
- package/query-planner/semantic-compat.js +35 -0
- package/query-planner/semantic-compiler.d.ts +64 -0
- package/query-planner/semantic-compiler.d.ts.map +1 -0
- package/query-planner/semantic-compiler.js +81 -0
- package/query-planner/semantic-literals.d.ts +30 -0
- package/query-planner/semantic-literals.d.ts.map +1 -0
- package/query-planner/semantic-literals.js +133 -0
- package/query-planner/semantic-types.d.ts +265 -0
- package/query-planner/semantic-types.d.ts.map +1 -0
- package/query-planner/semantic-types.js +1 -0
- package/query-planner/semantic.d.ts +54 -0
- package/query-planner/semantic.d.ts.map +1 -0
- package/query-planner/semantic.js +1421 -0
- package/query-planner/types.d.ts +560 -0
- package/query-planner/types.d.ts.map +1 -0
- package/query-planner/types.js +49 -0
- package/query-planner/wfs-semantic.d.ts +58 -0
- package/query-planner/wfs-semantic.d.ts.map +1 -0
- package/query-planner/wfs-semantic.js +547 -0
- package/query-planner/wfs.d.ts +6 -0
- package/query-planner/wfs.d.ts.map +1 -0
- package/query-planner/wfs.js +71 -0
- package/react/external-map.d.ts +67 -0
- package/react/external-map.d.ts.map +1 -0
- package/react/external-map.js +63 -0
- package/react/honua-map.d.ts.map +1 -1
- package/react/honua-map.js +3 -2
- package/react/honua-source-layer.d.ts +110 -0
- package/react/honua-source-layer.d.ts.map +1 -0
- package/react/honua-source-layer.js +101 -0
- package/react/hooks.d.ts +28 -0
- package/react/hooks.d.ts.map +1 -1
- package/react/hooks.js +26 -4
- package/react/index.d.ts +8 -0
- package/react/index.d.ts.map +1 -1
- package/react/index.js +5 -0
- package/react/query-cache.d.ts +26 -4
- package/react/query-cache.d.ts.map +1 -1
- package/react/query-cache.js +51 -8
- package/react/selection.d.ts +177 -0
- package/react/selection.d.ts.map +1 -0
- package/react/selection.js +0 -0
- package/react/use-mounted-source.d.ts +69 -0
- package/react/use-mounted-source.d.ts.map +1 -0
- package/react/use-mounted-source.js +346 -0
- package/runtime/edit-snapping.d.ts +107 -0
- package/runtime/edit-snapping.d.ts.map +1 -0
- package/runtime/edit-snapping.js +190 -0
- package/runtime/errors.d.ts +3 -2
- package/runtime/errors.d.ts.map +1 -1
- package/runtime/errors.js +19 -2
- package/runtime/index.d.ts +8 -2
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +4 -4
- package/runtime/load-package.d.ts.map +1 -1
- package/runtime/load-package.js +8 -0
- package/runtime/map-package-validation.d.ts +0 -13
- package/runtime/map-package-validation.d.ts.map +1 -1
- package/runtime/map-package-validation.js +0 -12
- package/runtime/map-package.d.ts +1 -1
- package/runtime/map-package.d.ts.map +1 -1
- package/runtime/maplibre-renderer.d.ts +41 -0
- package/runtime/maplibre-renderer.d.ts.map +1 -0
- package/runtime/maplibre-renderer.js +322 -0
- package/runtime/pmtiles-protocol.d.ts +77 -0
- package/runtime/pmtiles-protocol.d.ts.map +1 -0
- package/runtime/pmtiles-protocol.js +120 -0
- package/runtime/query-tiles.d.ts +2 -1
- package/runtime/query-tiles.d.ts.map +1 -1
- package/runtime/query-tiles.js +6 -2
- package/runtime/source-bridge.d.ts +4 -4
- package/runtime/source-bridge.d.ts.map +1 -1
- package/runtime/source-bridge.js +36 -56
- package/runtime/style-interactions.d.ts +3 -2
- package/runtime/style-interactions.d.ts.map +1 -1
- package/runtime/style-interactions.js +11 -3
- package/runtime/terra-draw-sketch.d.ts +275 -0
- package/runtime/terra-draw-sketch.d.ts.map +1 -0
- package/runtime/terra-draw-sketch.js +404 -0
- package/source-capability-registry.d.ts +1 -0
- package/source-capability-registry.js +1 -0
- package/source-capability-types.d.ts +186 -0
- package/source-capability-types.js +7 -0
- package/style/index.d.ts +2 -0
- package/style/index.d.ts.map +1 -1
- package/style/index.js +2 -0
- package/style/renderers.d.ts +326 -0
- package/style/renderers.d.ts.map +1 -0
- package/style/renderers.js +517 -0
- package/webmap/convert-renderer.d.ts +24 -3
- package/webmap/convert-renderer.d.ts.map +1 -1
- package/webmap/convert-renderer.js +93 -112
- package/webmap/index.d.ts +1 -1
- package/webmap/index.d.ts.map +1 -1
- package/webmap/index.js +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link CredentialStore} implementations.
|
|
3
|
+
*
|
|
4
|
+
* - {@link InMemoryCredentialStore} — the default. Tokens live only in the JS
|
|
5
|
+
* heap and vanish on reload; nothing else on the origin can read them.
|
|
6
|
+
* - {@link sessionStorageCredentialStore} / {@link localStorageCredentialStore}
|
|
7
|
+
* — explicit opt-in Web Storage adapters. Web Storage is readable by **any**
|
|
8
|
+
* script executing on the origin, so a single XSS bug can exfiltrate every
|
|
9
|
+
* stored token. Only enable them when you understand and accept that risk
|
|
10
|
+
* (see `docs/auth.md` → "Storage & security"). `localStorage` additionally
|
|
11
|
+
* persists across tabs/restarts, widening the exposure window.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Default credential store. Holds credentials in a process-local `Map` — never
|
|
17
|
+
* serialized, never shared across origins, cleared on reload.
|
|
18
|
+
*/
|
|
19
|
+
export class InMemoryCredentialStore {
|
|
20
|
+
entries = new Map();
|
|
21
|
+
get(key) {
|
|
22
|
+
return this.entries.get(key);
|
|
23
|
+
}
|
|
24
|
+
set(key, value) {
|
|
25
|
+
this.entries.set(key, value);
|
|
26
|
+
}
|
|
27
|
+
delete(key) {
|
|
28
|
+
this.entries.delete(key);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
class WebStorageCredentialStore {
|
|
32
|
+
namespace;
|
|
33
|
+
resolveStorage;
|
|
34
|
+
constructor(kind, options) {
|
|
35
|
+
this.namespace = options.namespace ?? "honua.auth";
|
|
36
|
+
this.resolveStorage = () => {
|
|
37
|
+
if (options.storage) {
|
|
38
|
+
return options.storage;
|
|
39
|
+
}
|
|
40
|
+
const storage = kind === "session" ? globalThis.sessionStorage : globalThis.localStorage;
|
|
41
|
+
if (!storage) {
|
|
42
|
+
throw new Error(`${kind}Storage is unavailable in this environment. Pass an explicit \`storage\` adapter or use the in-memory store.`);
|
|
43
|
+
}
|
|
44
|
+
return storage;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
storageKey(key) {
|
|
48
|
+
return `${this.namespace}:${key}`;
|
|
49
|
+
}
|
|
50
|
+
get(key) {
|
|
51
|
+
const raw = this.resolveStorage().getItem(this.storageKey(key));
|
|
52
|
+
if (!raw) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
set(key, value) {
|
|
63
|
+
this.resolveStorage().setItem(this.storageKey(key), JSON.stringify(value));
|
|
64
|
+
}
|
|
65
|
+
delete(key) {
|
|
66
|
+
this.resolveStorage().removeItem(this.storageKey(key));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `sessionStorage`-backed credential store (per-tab, cleared when the tab
|
|
71
|
+
* closes). Opt-in: see the module-level security note before using it.
|
|
72
|
+
*/
|
|
73
|
+
export function sessionStorageCredentialStore(options = {}) {
|
|
74
|
+
return new WebStorageCredentialStore("session", options);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `localStorage`-backed credential store (persists across tabs and restarts).
|
|
78
|
+
* Highest-risk option — opt-in only. See the module-level security note.
|
|
79
|
+
*/
|
|
80
|
+
export function localStorageCredentialStore(options = {}) {
|
|
81
|
+
return new WebStorageCredentialStore("local", options);
|
|
82
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@honua/sdk-js/auth` — pluggable {@link HonuaAuthProvider} built-ins and the
|
|
3
|
+
* {@link CredentialStore} abstraction.
|
|
4
|
+
*
|
|
5
|
+
* These providers plug into `new HonuaClient({ auth })` and are therefore
|
|
6
|
+
* attached and refreshed transparently across **all** transports the client
|
|
7
|
+
* drives — every REST fetch path and every gRPC-web call (the client's auth
|
|
8
|
+
* interceptor re-resolves credentials per attempt), plus the 401/403
|
|
9
|
+
* force-refresh retry.
|
|
10
|
+
*
|
|
11
|
+
* This module is a dedicated subpath so a `HonuaClient`-only consumer never
|
|
12
|
+
* pays the PKCE/token-exchange code size unless they opt in.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
export { oauth2, OAuth2Provider } from "./oauth2-provider.js";
|
|
17
|
+
export { clientCredentials, ClientCredentialsProvider } from "./client-credentials-provider.js";
|
|
18
|
+
export { apiKeyAuth, bearerTokenAuth } from "./static-providers.js";
|
|
19
|
+
export { InMemoryCredentialStore, sessionStorageCredentialStore, localStorageCredentialStore, } from "./credential-store.js";
|
|
20
|
+
export { createPkcePair, createStateToken } from "./pkce.js";
|
|
21
|
+
export type { PkcePair } from "./pkce.js";
|
|
22
|
+
export type { ClientCredentialsConfig, CredentialStore, HonuaAuthEvent, HonuaAuthEventHandle, HonuaAuthEventListener, OAuth2Config, OAuth2SignInMode, OAuth2TokenResponse, StoredCredential, } from "./types.js";
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAChG,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,2BAA2B,GAC5B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7D,YAAY,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC1C,YAAY,EACV,uBAAuB,EACvB,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@honua/sdk-js/auth` — pluggable {@link HonuaAuthProvider} built-ins and the
|
|
3
|
+
* {@link CredentialStore} abstraction.
|
|
4
|
+
*
|
|
5
|
+
* These providers plug into `new HonuaClient({ auth })` and are therefore
|
|
6
|
+
* attached and refreshed transparently across **all** transports the client
|
|
7
|
+
* drives — every REST fetch path and every gRPC-web call (the client's auth
|
|
8
|
+
* interceptor re-resolves credentials per attempt), plus the 401/403
|
|
9
|
+
* force-refresh retry.
|
|
10
|
+
*
|
|
11
|
+
* This module is a dedicated subpath so a `HonuaClient`-only consumer never
|
|
12
|
+
* pays the PKCE/token-exchange code size unless they opt in.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
export { oauth2, OAuth2Provider } from "./oauth2-provider.js";
|
|
17
|
+
export { clientCredentials, ClientCredentialsProvider } from "./client-credentials-provider.js";
|
|
18
|
+
export { apiKeyAuth, bearerTokenAuth } from "./static-providers.js";
|
|
19
|
+
export { InMemoryCredentialStore, sessionStorageCredentialStore, localStorageCredentialStore, } from "./credential-store.js";
|
|
20
|
+
export { createPkcePair, createStateToken } from "./pkce.js";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth2 authorization-code + PKCE provider.
|
|
3
|
+
*
|
|
4
|
+
* Implements {@link HonuaAuthProvider} (so it drops straight into
|
|
5
|
+
* `new HonuaClient({ auth })` and is attached/refreshed on every fetch and
|
|
6
|
+
* gRPC-web call) *and* exposes the interactive surface a browser app drives:
|
|
7
|
+
* `signIn()` (redirect or popup), `handleRedirectCallback()`, `signOut()`.
|
|
8
|
+
*
|
|
9
|
+
* Design highlights:
|
|
10
|
+
* - S256 PKCE + unguessable `state` (CSRF), validated on the redirect.
|
|
11
|
+
* - Silent refresh via `refresh_token` with a documented fallback
|
|
12
|
+
* (`interaction_required`) when no refresh token is available.
|
|
13
|
+
* - Clock-skew-tolerant expiry so a refresh happens *before* requests 401.
|
|
14
|
+
* - **Single-flight refresh**: N concurrent `getCredentials()` calls that all
|
|
15
|
+
* observe an expiring token share exactly one token-endpoint round-trip.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
import type { HonuaAuthCredentials, HonuaAuthProviderContext, HonuaAuthRevocationContext } from "../types.js";
|
|
20
|
+
import type { HonuaAuthEventHandle, HonuaAuthEventListener, OAuth2Config, StoredCredential } from "./types.js";
|
|
21
|
+
/**
|
|
22
|
+
* OAuth2 authorization-code + PKCE provider. Construct via {@link oauth2}.
|
|
23
|
+
*/
|
|
24
|
+
export declare class OAuth2Provider {
|
|
25
|
+
private readonly config;
|
|
26
|
+
private readonly store;
|
|
27
|
+
private readonly serviceKey;
|
|
28
|
+
private readonly clockSkewMs;
|
|
29
|
+
private readonly mode;
|
|
30
|
+
private readonly fetchFn;
|
|
31
|
+
private readonly listeners;
|
|
32
|
+
/** Single-flight guard: one in-flight refresh shared by all callers. */
|
|
33
|
+
private refreshInFlight;
|
|
34
|
+
/** In-memory transaction fallback when no Web Storage is available. */
|
|
35
|
+
private memoryTransaction;
|
|
36
|
+
constructor(config: OAuth2Config);
|
|
37
|
+
/**
|
|
38
|
+
* Resolve credentials for an outgoing request. Returns the current access
|
|
39
|
+
* token, silently refreshing it first when it is expiring (or when
|
|
40
|
+
* `context.forceRefresh` is set, e.g. after a 401). Throws
|
|
41
|
+
* {@link HonuaAuthError} `interaction_required` when there is no token and no
|
|
42
|
+
* refresh token — the app must call {@link OAuth2Provider.signIn}.
|
|
43
|
+
*/
|
|
44
|
+
getCredentials(context?: HonuaAuthProviderContext): Promise<HonuaAuthCredentials | undefined>;
|
|
45
|
+
/** {@link HonuaAuthProvider.revokeCredentials} — clears + optionally revokes. */
|
|
46
|
+
revokeCredentials(_credentials: HonuaAuthCredentials, _context: HonuaAuthRevocationContext): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Start interactive sign-in. In `redirect` mode this navigates the current
|
|
49
|
+
* window to the authorization endpoint and returns a promise that never
|
|
50
|
+
* resolves (the page unloads); complete the flow by calling
|
|
51
|
+
* {@link OAuth2Provider.handleRedirectCallback} when the app reloads at the
|
|
52
|
+
* redirect URI. In `popup` mode it opens a popup, waits for the redirect back,
|
|
53
|
+
* exchanges the code, and resolves with the stored credential.
|
|
54
|
+
*/
|
|
55
|
+
signIn(): Promise<StoredCredential | undefined>;
|
|
56
|
+
/**
|
|
57
|
+
* Whether `url` (or the current window location) looks like an OAuth2 redirect
|
|
58
|
+
* callback (carries `code`+`state` or an `error`). Call this on app load to
|
|
59
|
+
* decide whether to invoke {@link OAuth2Provider.handleRedirectCallback}.
|
|
60
|
+
*/
|
|
61
|
+
isRedirectCallback(url?: string): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Complete the redirect leg: validate `state`, exchange the authorization
|
|
64
|
+
* `code` (+ PKCE verifier) for tokens, persist them, and emit `signed-in`.
|
|
65
|
+
*/
|
|
66
|
+
handleRedirectCallback(url?: string): Promise<StoredCredential>;
|
|
67
|
+
/** Whether a (non-expired) credential is currently stored. */
|
|
68
|
+
isSignedIn(): Promise<boolean>;
|
|
69
|
+
/** Read the raw stored credential, if any. */
|
|
70
|
+
getStoredCredential(): StoredCredential | undefined | Promise<StoredCredential | undefined>;
|
|
71
|
+
/** Clear stored credentials, optionally revoke them, and emit `signed-out`. */
|
|
72
|
+
signOut(): Promise<void>;
|
|
73
|
+
/** Register an auth lifecycle listener. */
|
|
74
|
+
on(listener: HonuaAuthEventListener): HonuaAuthEventHandle;
|
|
75
|
+
private refreshSingleFlight;
|
|
76
|
+
private doRefresh;
|
|
77
|
+
private exchangeCode;
|
|
78
|
+
private revokeAtEndpoint;
|
|
79
|
+
private buildAuthorizeUrl;
|
|
80
|
+
private signInPopup;
|
|
81
|
+
private parseUrl;
|
|
82
|
+
private getWindow;
|
|
83
|
+
private requireWindow;
|
|
84
|
+
private transactionKey;
|
|
85
|
+
private sessionStorage;
|
|
86
|
+
private saveTransaction;
|
|
87
|
+
private loadTransaction;
|
|
88
|
+
private clearTransaction;
|
|
89
|
+
private emit;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create an OAuth2 authorization-code + PKCE {@link HonuaAuthProvider}.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { HonuaClient } from "@honua/sdk-js/honua";
|
|
97
|
+
* import { oauth2 } from "@honua/sdk-js/auth";
|
|
98
|
+
*
|
|
99
|
+
* const auth = oauth2({
|
|
100
|
+
* authorizationEndpoint: "https://idp.example/authorize",
|
|
101
|
+
* tokenEndpoint: "https://idp.example/token",
|
|
102
|
+
* clientId: "my-app",
|
|
103
|
+
* redirectUri: `${location.origin}/callback`,
|
|
104
|
+
* scopes: ["openid", "profile"],
|
|
105
|
+
* });
|
|
106
|
+
* const client = new HonuaClient({ baseUrl: "https://api.example", auth });
|
|
107
|
+
*
|
|
108
|
+
* // On the callback page:
|
|
109
|
+
* if (auth.isRedirectCallback()) await auth.handleRedirectCallback();
|
|
110
|
+
* else if (!(await auth.isSignedIn())) await auth.signIn();
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export declare function oauth2(config: OAuth2Config): OAuth2Provider;
|
|
114
|
+
//# sourceMappingURL=oauth2-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth2-provider.d.ts","sourceRoot":"","sources":["../../../../src/core/auth/oauth2-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAU9G,OAAO,KAAK,EAGV,oBAAoB,EACpB,sBAAsB,EACtB,YAAY,EAEZ,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAWpB;;GAEG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,wEAAwE;IACxE,OAAO,CAAC,eAAe,CAAwC;IAC/D,uEAAuE;IACvE,OAAO,CAAC,iBAAiB,CAA8B;gBAEpC,MAAM,EAAE,YAAY;IAcvC;;;;;;OAMG;IACU,cAAc,CACzB,OAAO,GAAE,wBAAoE,GAC5E,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAkB5C,iFAAiF;IACpE,iBAAiB,CAC5B,YAAY,EAAE,oBAAoB,EAClC,QAAQ,EAAE,0BAA0B,GACnC,OAAO,CAAC,IAAI,CAAC;IAMhB;;;;;;;OAOG;IACU,MAAM,IAAI,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAsB5D;;;;OAIG;IACI,kBAAkB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO;IAQhD;;;OAGG;IACU,sBAAsB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAsC5E,8DAA8D;IACjD,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAK3C,8CAA8C;IACvC,mBAAmB,IAAI,gBAAgB,GAAG,SAAS,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAIlG,+EAA+E;IAClE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAarC,2CAA2C;IACpC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,GAAG,oBAAoB;IAWjE,OAAO,CAAC,mBAAmB;YAab,SAAS;YAmCT,YAAY;YAeZ,gBAAgB;IAa9B,OAAO,CAAC,iBAAiB;YAkBX,WAAW;IA6CzB,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,eAAe;IAgBvB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,IAAI;CASb;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc,CAE3D"}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth2 authorization-code + PKCE provider.
|
|
3
|
+
*
|
|
4
|
+
* Implements {@link HonuaAuthProvider} (so it drops straight into
|
|
5
|
+
* `new HonuaClient({ auth })` and is attached/refreshed on every fetch and
|
|
6
|
+
* gRPC-web call) *and* exposes the interactive surface a browser app drives:
|
|
7
|
+
* `signIn()` (redirect or popup), `handleRedirectCallback()`, `signOut()`.
|
|
8
|
+
*
|
|
9
|
+
* Design highlights:
|
|
10
|
+
* - S256 PKCE + unguessable `state` (CSRF), validated on the redirect.
|
|
11
|
+
* - Silent refresh via `refresh_token` with a documented fallback
|
|
12
|
+
* (`interaction_required`) when no refresh token is available.
|
|
13
|
+
* - Clock-skew-tolerant expiry so a refresh happens *before* requests 401.
|
|
14
|
+
* - **Single-flight refresh**: N concurrent `getCredentials()` calls that all
|
|
15
|
+
* observe an expiring token share exactly one token-endpoint round-trip.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
import { HonuaAuthError } from "../errors.js";
|
|
20
|
+
import { InMemoryCredentialStore } from "./credential-store.js";
|
|
21
|
+
import { createPkcePair, createStateToken } from "./pkce.js";
|
|
22
|
+
import { DEFAULT_CLOCK_SKEW_MS, isCredentialExpiring, requestToken, resolveServiceKey, toHonuaCredentials, } from "./token-endpoint.js";
|
|
23
|
+
const TRANSACTION_NAMESPACE = "honua.auth.txn";
|
|
24
|
+
/**
|
|
25
|
+
* OAuth2 authorization-code + PKCE provider. Construct via {@link oauth2}.
|
|
26
|
+
*/
|
|
27
|
+
export class OAuth2Provider {
|
|
28
|
+
config;
|
|
29
|
+
store;
|
|
30
|
+
serviceKey;
|
|
31
|
+
clockSkewMs;
|
|
32
|
+
mode;
|
|
33
|
+
fetchFn;
|
|
34
|
+
listeners = new Set();
|
|
35
|
+
/** Single-flight guard: one in-flight refresh shared by all callers. */
|
|
36
|
+
refreshInFlight;
|
|
37
|
+
/** In-memory transaction fallback when no Web Storage is available. */
|
|
38
|
+
memoryTransaction;
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
this.store = config.store ?? new InMemoryCredentialStore();
|
|
42
|
+
this.serviceKey = resolveServiceKey(config.serviceKey, config.clientId, config.tokenEndpoint);
|
|
43
|
+
this.clockSkewMs = config.clockSkewMs ?? DEFAULT_CLOCK_SKEW_MS;
|
|
44
|
+
this.mode = config.mode ?? "redirect";
|
|
45
|
+
this.fetchFn = (config.fetchFn ?? fetch).bind(globalThis);
|
|
46
|
+
if (config.onEvent) {
|
|
47
|
+
this.listeners.add(config.onEvent);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ── HonuaAuthProvider surface ────────────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* Resolve credentials for an outgoing request. Returns the current access
|
|
53
|
+
* token, silently refreshing it first when it is expiring (or when
|
|
54
|
+
* `context.forceRefresh` is set, e.g. after a 401). Throws
|
|
55
|
+
* {@link HonuaAuthError} `interaction_required` when there is no token and no
|
|
56
|
+
* refresh token — the app must call {@link OAuth2Provider.signIn}.
|
|
57
|
+
*/
|
|
58
|
+
async getCredentials(context = { reason: "manual", forceRefresh: false }) {
|
|
59
|
+
const current = await this.store.get(this.serviceKey);
|
|
60
|
+
const needsRefresh = context.forceRefresh || isCredentialExpiring(current, this.clockSkewMs);
|
|
61
|
+
if (!needsRefresh && current) {
|
|
62
|
+
return toHonuaCredentials(current);
|
|
63
|
+
}
|
|
64
|
+
if (current?.refreshToken) {
|
|
65
|
+
const refreshed = await this.refreshSingleFlight(current);
|
|
66
|
+
return toHonuaCredentials(refreshed);
|
|
67
|
+
}
|
|
68
|
+
throw new HonuaAuthError("interaction_required", current
|
|
69
|
+
? "Access token expired and no refresh token is available; call signIn() to re-authenticate."
|
|
70
|
+
: "No credential available; call signIn() to start the OAuth2 authorization-code flow.");
|
|
71
|
+
}
|
|
72
|
+
/** {@link HonuaAuthProvider.revokeCredentials} — clears + optionally revokes. */
|
|
73
|
+
async revokeCredentials(_credentials, _context) {
|
|
74
|
+
await this.signOut();
|
|
75
|
+
}
|
|
76
|
+
// ── Interactive flow ─────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* Start interactive sign-in. In `redirect` mode this navigates the current
|
|
79
|
+
* window to the authorization endpoint and returns a promise that never
|
|
80
|
+
* resolves (the page unloads); complete the flow by calling
|
|
81
|
+
* {@link OAuth2Provider.handleRedirectCallback} when the app reloads at the
|
|
82
|
+
* redirect URI. In `popup` mode it opens a popup, waits for the redirect back,
|
|
83
|
+
* exchanges the code, and resolves with the stored credential.
|
|
84
|
+
*/
|
|
85
|
+
async signIn() {
|
|
86
|
+
const pkce = await createPkcePair();
|
|
87
|
+
const state = createStateToken();
|
|
88
|
+
const transaction = {
|
|
89
|
+
codeVerifier: pkce.codeVerifier,
|
|
90
|
+
state,
|
|
91
|
+
redirectUri: this.config.redirectUri,
|
|
92
|
+
createdAt: Date.now(),
|
|
93
|
+
};
|
|
94
|
+
const authorizeUrl = this.buildAuthorizeUrl(pkce, state);
|
|
95
|
+
if (this.mode === "popup") {
|
|
96
|
+
return this.signInPopup(authorizeUrl, transaction);
|
|
97
|
+
}
|
|
98
|
+
this.saveTransaction(transaction);
|
|
99
|
+
const win = this.requireWindow();
|
|
100
|
+
win.location.assign(authorizeUrl);
|
|
101
|
+
// The redirect unloads the page; never resolves.
|
|
102
|
+
return new Promise(() => { });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether `url` (or the current window location) looks like an OAuth2 redirect
|
|
106
|
+
* callback (carries `code`+`state` or an `error`). Call this on app load to
|
|
107
|
+
* decide whether to invoke {@link OAuth2Provider.handleRedirectCallback}.
|
|
108
|
+
*/
|
|
109
|
+
isRedirectCallback(url) {
|
|
110
|
+
const target = this.parseUrl(url);
|
|
111
|
+
if (!target) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return target.searchParams.has("error") || (target.searchParams.has("code") && target.searchParams.has("state"));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Complete the redirect leg: validate `state`, exchange the authorization
|
|
118
|
+
* `code` (+ PKCE verifier) for tokens, persist them, and emit `signed-in`.
|
|
119
|
+
*/
|
|
120
|
+
async handleRedirectCallback(url) {
|
|
121
|
+
const target = this.parseUrl(url);
|
|
122
|
+
if (!target) {
|
|
123
|
+
throw new HonuaAuthError("interaction_required", "No redirect callback URL is available to process.");
|
|
124
|
+
}
|
|
125
|
+
const params = target.searchParams;
|
|
126
|
+
const authError = params.get("error");
|
|
127
|
+
if (authError) {
|
|
128
|
+
const description = params.get("error_description");
|
|
129
|
+
this.clearTransaction();
|
|
130
|
+
throw new HonuaAuthError("invalid_grant", `Authorization server returned an error: ${authError}${description ? ` (${description})` : ""}`);
|
|
131
|
+
}
|
|
132
|
+
const code = params.get("code");
|
|
133
|
+
const state = params.get("state");
|
|
134
|
+
if (!code) {
|
|
135
|
+
throw new HonuaAuthError("interaction_required", "Redirect callback is missing the authorization code.");
|
|
136
|
+
}
|
|
137
|
+
const transaction = this.loadTransaction();
|
|
138
|
+
if (!transaction) {
|
|
139
|
+
throw new HonuaAuthError("interaction_required", "No PKCE transaction is in progress; start sign-in with signIn() first.");
|
|
140
|
+
}
|
|
141
|
+
if (!state || state !== transaction.state) {
|
|
142
|
+
this.clearTransaction();
|
|
143
|
+
throw new HonuaAuthError("invalid_grant", "OAuth2 state mismatch on redirect callback (possible CSRF).");
|
|
144
|
+
}
|
|
145
|
+
const credential = await this.exchangeCode(code, transaction);
|
|
146
|
+
await this.store.set(this.serviceKey, credential);
|
|
147
|
+
this.clearTransaction();
|
|
148
|
+
this.emit({ type: "signed-in", credential });
|
|
149
|
+
return credential;
|
|
150
|
+
}
|
|
151
|
+
/** Whether a (non-expired) credential is currently stored. */
|
|
152
|
+
async isSignedIn() {
|
|
153
|
+
const current = await this.store.get(this.serviceKey);
|
|
154
|
+
return current !== undefined && !isCredentialExpiring(current, 0);
|
|
155
|
+
}
|
|
156
|
+
/** Read the raw stored credential, if any. */
|
|
157
|
+
getStoredCredential() {
|
|
158
|
+
return this.store.get(this.serviceKey);
|
|
159
|
+
}
|
|
160
|
+
/** Clear stored credentials, optionally revoke them, and emit `signed-out`. */
|
|
161
|
+
async signOut() {
|
|
162
|
+
const current = await this.store.get(this.serviceKey);
|
|
163
|
+
await this.store.delete(this.serviceKey);
|
|
164
|
+
this.clearTransaction();
|
|
165
|
+
this.refreshInFlight = undefined;
|
|
166
|
+
if (current && this.config.revocationEndpoint) {
|
|
167
|
+
await this.revokeAtEndpoint(current).catch(() => {
|
|
168
|
+
// Best-effort revocation; local sign-out already succeeded.
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
this.emit({ type: "signed-out" });
|
|
172
|
+
}
|
|
173
|
+
/** Register an auth lifecycle listener. */
|
|
174
|
+
on(listener) {
|
|
175
|
+
this.listeners.add(listener);
|
|
176
|
+
return {
|
|
177
|
+
remove: () => {
|
|
178
|
+
this.listeners.delete(listener);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// ── internals ────────────────────────────────────────────────
|
|
183
|
+
refreshSingleFlight(current) {
|
|
184
|
+
if (this.refreshInFlight) {
|
|
185
|
+
return this.refreshInFlight;
|
|
186
|
+
}
|
|
187
|
+
const inFlight = this.doRefresh(current).finally(() => {
|
|
188
|
+
if (this.refreshInFlight === inFlight) {
|
|
189
|
+
this.refreshInFlight = undefined;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
this.refreshInFlight = inFlight;
|
|
193
|
+
return inFlight;
|
|
194
|
+
}
|
|
195
|
+
async doRefresh(current) {
|
|
196
|
+
const refreshToken = current.refreshToken;
|
|
197
|
+
if (!refreshToken) {
|
|
198
|
+
throw new HonuaAuthError("interaction_required", "No refresh token is available; call signIn().");
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const refreshed = await requestToken({
|
|
202
|
+
fetchFn: this.fetchFn,
|
|
203
|
+
tokenEndpoint: this.config.tokenEndpoint,
|
|
204
|
+
params: {
|
|
205
|
+
grant_type: "refresh_token",
|
|
206
|
+
refresh_token: refreshToken,
|
|
207
|
+
client_id: this.config.clientId,
|
|
208
|
+
...(this.config.scopes && this.config.scopes.length > 0 ? { scope: this.config.scopes.join(" ") } : {}),
|
|
209
|
+
...this.config.extraTokenParams,
|
|
210
|
+
},
|
|
211
|
+
previous: current,
|
|
212
|
+
});
|
|
213
|
+
await this.store.set(this.serviceKey, refreshed);
|
|
214
|
+
this.emit({ type: "token-refreshed", credential: refreshed });
|
|
215
|
+
return refreshed;
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (error instanceof HonuaAuthError) {
|
|
219
|
+
this.emit({ type: "refresh-failed", error });
|
|
220
|
+
if (error.code === "invalid_grant") {
|
|
221
|
+
// The refresh token is dead; drop it so the next call surfaces
|
|
222
|
+
// interaction_required instead of looping on a doomed refresh.
|
|
223
|
+
await this.store.delete(this.serviceKey);
|
|
224
|
+
this.emit({ type: "signed-out" });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async exchangeCode(code, transaction) {
|
|
231
|
+
return requestToken({
|
|
232
|
+
fetchFn: this.fetchFn,
|
|
233
|
+
tokenEndpoint: this.config.tokenEndpoint,
|
|
234
|
+
params: {
|
|
235
|
+
grant_type: "authorization_code",
|
|
236
|
+
code,
|
|
237
|
+
redirect_uri: transaction.redirectUri,
|
|
238
|
+
client_id: this.config.clientId,
|
|
239
|
+
code_verifier: transaction.codeVerifier,
|
|
240
|
+
...this.config.extraTokenParams,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async revokeAtEndpoint(credential) {
|
|
245
|
+
const endpoint = this.config.revocationEndpoint;
|
|
246
|
+
if (!endpoint) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const token = credential.refreshToken ?? credential.accessToken;
|
|
250
|
+
await this.fetchFn(endpoint, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
253
|
+
body: new URLSearchParams({ token, client_id: this.config.clientId }).toString(),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
buildAuthorizeUrl(pkce, state) {
|
|
257
|
+
const url = new URL(this.config.authorizationEndpoint);
|
|
258
|
+
const params = url.searchParams;
|
|
259
|
+
params.set("response_type", "code");
|
|
260
|
+
params.set("client_id", this.config.clientId);
|
|
261
|
+
params.set("redirect_uri", this.config.redirectUri);
|
|
262
|
+
params.set("state", state);
|
|
263
|
+
params.set("code_challenge", pkce.codeChallenge);
|
|
264
|
+
params.set("code_challenge_method", pkce.codeChallengeMethod);
|
|
265
|
+
if (this.config.scopes && this.config.scopes.length > 0) {
|
|
266
|
+
params.set("scope", this.config.scopes.join(" "));
|
|
267
|
+
}
|
|
268
|
+
for (const [key, value] of Object.entries(this.config.extraAuthorizationParams ?? {})) {
|
|
269
|
+
params.set(key, value);
|
|
270
|
+
}
|
|
271
|
+
return url.toString();
|
|
272
|
+
}
|
|
273
|
+
async signInPopup(authorizeUrl, transaction) {
|
|
274
|
+
const win = this.requireWindow();
|
|
275
|
+
const popup = win.open(authorizeUrl, "honua-oauth", this.config.popupFeatures ?? "width=480,height=680");
|
|
276
|
+
if (!popup) {
|
|
277
|
+
throw new HonuaAuthError("interaction_required", "The sign-in popup was blocked by the browser.");
|
|
278
|
+
}
|
|
279
|
+
const redirectOrigin = new URL(transaction.redirectUri, win.location?.href ?? transaction.redirectUri).origin;
|
|
280
|
+
const callback = await new Promise((resolve, reject) => {
|
|
281
|
+
const timer = setInterval(() => {
|
|
282
|
+
try {
|
|
283
|
+
if (popup.closed) {
|
|
284
|
+
clearInterval(timer);
|
|
285
|
+
reject(new HonuaAuthError("interaction_required", "The sign-in popup was closed before completing sign-in."));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
let popupUrl;
|
|
289
|
+
try {
|
|
290
|
+
popupUrl = new URL(popup.location.href);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Cross-origin while on the authorization server — not readable yet.
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (popupUrl.origin === redirectOrigin && popupUrl.searchParams.has("code")) {
|
|
297
|
+
clearInterval(timer);
|
|
298
|
+
const code = popupUrl.searchParams.get("code") ?? "";
|
|
299
|
+
const state = popupUrl.searchParams.get("state") ?? "";
|
|
300
|
+
popup.close();
|
|
301
|
+
resolve({ code, state });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// Ignore transient cross-origin access errors during the auth leg.
|
|
306
|
+
}
|
|
307
|
+
}, 200);
|
|
308
|
+
});
|
|
309
|
+
if (!callback.state || callback.state !== transaction.state) {
|
|
310
|
+
throw new HonuaAuthError("invalid_grant", "OAuth2 state mismatch on popup callback (possible CSRF).");
|
|
311
|
+
}
|
|
312
|
+
const credential = await this.exchangeCode(callback.code, transaction);
|
|
313
|
+
await this.store.set(this.serviceKey, credential);
|
|
314
|
+
this.emit({ type: "signed-in", credential });
|
|
315
|
+
return credential;
|
|
316
|
+
}
|
|
317
|
+
parseUrl(url) {
|
|
318
|
+
const href = url ?? this.getWindow()?.location?.href;
|
|
319
|
+
if (!href) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
return new URL(href);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
getWindow() {
|
|
330
|
+
return this.config.windowRef ?? globalThis.window;
|
|
331
|
+
}
|
|
332
|
+
requireWindow() {
|
|
333
|
+
const win = this.getWindow();
|
|
334
|
+
if (!win) {
|
|
335
|
+
throw new HonuaAuthError("interaction_required", "Interactive OAuth2 sign-in requires a browser window; none is available in this environment.");
|
|
336
|
+
}
|
|
337
|
+
return win;
|
|
338
|
+
}
|
|
339
|
+
transactionKey() {
|
|
340
|
+
return `${TRANSACTION_NAMESPACE}:${this.serviceKey}`;
|
|
341
|
+
}
|
|
342
|
+
sessionStorage() {
|
|
343
|
+
return this.getWindow()?.sessionStorage;
|
|
344
|
+
}
|
|
345
|
+
saveTransaction(transaction) {
|
|
346
|
+
const storage = this.sessionStorage();
|
|
347
|
+
if (storage) {
|
|
348
|
+
storage.setItem(this.transactionKey(), JSON.stringify(transaction));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.memoryTransaction = transaction;
|
|
352
|
+
}
|
|
353
|
+
loadTransaction() {
|
|
354
|
+
const storage = this.sessionStorage();
|
|
355
|
+
if (storage) {
|
|
356
|
+
const raw = storage.getItem(this.transactionKey());
|
|
357
|
+
if (!raw) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
return JSON.parse(raw);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return this.memoryTransaction;
|
|
368
|
+
}
|
|
369
|
+
clearTransaction() {
|
|
370
|
+
this.sessionStorage()?.removeItem(this.transactionKey());
|
|
371
|
+
this.memoryTransaction = undefined;
|
|
372
|
+
}
|
|
373
|
+
emit(event) {
|
|
374
|
+
for (const listener of this.listeners) {
|
|
375
|
+
try {
|
|
376
|
+
listener(event);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// Listener errors must not break the auth flow.
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Create an OAuth2 authorization-code + PKCE {@link HonuaAuthProvider}.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* ```ts
|
|
389
|
+
* import { HonuaClient } from "@honua/sdk-js/honua";
|
|
390
|
+
* import { oauth2 } from "@honua/sdk-js/auth";
|
|
391
|
+
*
|
|
392
|
+
* const auth = oauth2({
|
|
393
|
+
* authorizationEndpoint: "https://idp.example/authorize",
|
|
394
|
+
* tokenEndpoint: "https://idp.example/token",
|
|
395
|
+
* clientId: "my-app",
|
|
396
|
+
* redirectUri: `${location.origin}/callback`,
|
|
397
|
+
* scopes: ["openid", "profile"],
|
|
398
|
+
* });
|
|
399
|
+
* const client = new HonuaClient({ baseUrl: "https://api.example", auth });
|
|
400
|
+
*
|
|
401
|
+
* // On the callback page:
|
|
402
|
+
* if (auth.isRedirectCallback()) await auth.handleRedirectCallback();
|
|
403
|
+
* else if (!(await auth.isSignedIn())) await auth.signIn();
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
export function oauth2(config) {
|
|
407
|
+
return new OAuth2Provider(config);
|
|
408
|
+
}
|