@pellux/goodvibes-sdk 0.19.6 → 0.19.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/contracts/index.d.ts +1 -0
- package/dist/_internal/contracts/index.d.ts.map +1 -1
- package/dist/_internal/contracts/index.js +2 -0
- package/dist/_internal/contracts/types.d.ts +4 -0
- package/dist/_internal/contracts/types.d.ts.map +1 -1
- package/dist/_internal/contracts/zod-schemas/accounts.d.ts +81 -0
- package/dist/_internal/contracts/zod-schemas/accounts.d.ts.map +1 -0
- package/dist/_internal/contracts/zod-schemas/accounts.js +47 -0
- package/dist/_internal/contracts/zod-schemas/auth.d.ts +42 -0
- package/dist/_internal/contracts/zod-schemas/auth.d.ts.map +1 -0
- package/dist/_internal/contracts/zod-schemas/auth.js +29 -0
- package/dist/_internal/contracts/zod-schemas/events.d.ts +37 -0
- package/dist/_internal/contracts/zod-schemas/events.d.ts.map +1 -0
- package/dist/_internal/contracts/zod-schemas/events.js +26 -0
- package/dist/_internal/contracts/zod-schemas/index.d.ts +9 -0
- package/dist/_internal/contracts/zod-schemas/index.d.ts.map +1 -0
- package/dist/_internal/contracts/zod-schemas/index.js +4 -0
- package/dist/_internal/contracts/zod-schemas/session.d.ts +22 -0
- package/dist/_internal/contracts/zod-schemas/session.d.ts.map +1 -0
- package/dist/_internal/contracts/zod-schemas/session.js +19 -0
- package/dist/_internal/daemon/api-router.d.ts.map +1 -1
- package/dist/_internal/daemon/api-router.js +0 -1
- package/dist/_internal/daemon/automation.d.ts.map +1 -1
- package/dist/_internal/daemon/channel-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/channel-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/context.d.ts.map +1 -1
- package/dist/_internal/daemon/control-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/http-policy.d.ts.map +1 -1
- package/dist/_internal/daemon/http-policy.js +0 -1
- package/dist/_internal/daemon/integration-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/integration-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/knowledge-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/knowledge-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/knowledge-routes.js +5 -4
- package/dist/_internal/daemon/media-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/media-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/operator.d.ts.map +1 -1
- package/dist/_internal/daemon/remote-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/remote.d.ts.map +1 -1
- package/dist/_internal/daemon/route-helpers.d.ts.map +1 -1
- package/dist/_internal/daemon/runtime-automation-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/runtime-route-types.d.ts +14 -1
- package/dist/_internal/daemon/runtime-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/runtime-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/runtime-session-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/runtime-session-routes.js +0 -2
- package/dist/_internal/daemon/sessions.d.ts.map +1 -1
- package/dist/_internal/daemon/system-route-types.d.ts.map +1 -1
- package/dist/_internal/daemon/system-routes.d.ts.map +1 -1
- package/dist/_internal/daemon/tasks.d.ts.map +1 -1
- package/dist/_internal/daemon/telemetry-routes.d.ts.map +1 -1
- package/dist/_internal/errors/daemon-error-contract.d.ts.map +1 -1
- package/dist/_internal/errors/index.d.ts +2 -2
- package/dist/_internal/errors/index.js +2 -2
- package/dist/_internal/operator/client-core.d.ts.map +1 -1
- package/dist/_internal/operator/client-core.js +8 -2
- package/dist/_internal/operator/client.d.ts +7 -0
- package/dist/_internal/operator/client.d.ts.map +1 -1
- package/dist/_internal/operator/client.js +32 -1
- package/dist/_internal/peer/client-core.d.ts.map +1 -1
- package/dist/_internal/platform/agents/orchestrator.d.ts +7 -0
- package/dist/_internal/platform/agents/orchestrator.d.ts.map +1 -1
- package/dist/_internal/platform/agents/orchestrator.js +8 -0
- package/dist/_internal/platform/auth/android-keystore-token-store.d.ts +110 -0
- package/dist/_internal/platform/auth/android-keystore-token-store.d.ts.map +1 -0
- package/dist/_internal/platform/auth/android-keystore-token-store.js +164 -0
- package/dist/_internal/platform/auth/auto-refresh-middleware.d.ts +46 -0
- package/dist/_internal/platform/auth/auto-refresh-middleware.d.ts.map +1 -0
- package/dist/_internal/platform/auth/auto-refresh-middleware.js +155 -0
- package/dist/_internal/platform/auth/auto-refresh.d.ts +123 -0
- package/dist/_internal/platform/auth/auto-refresh.d.ts.map +1 -0
- package/dist/_internal/platform/auth/auto-refresh.js +236 -0
- package/dist/_internal/platform/auth/expo-secure-token-store.d.ts +82 -0
- package/dist/_internal/platform/auth/expo-secure-token-store.d.ts.map +1 -0
- package/dist/_internal/platform/auth/expo-secure-token-store.js +135 -0
- package/dist/_internal/platform/auth/index.d.ts +3 -0
- package/dist/_internal/platform/auth/index.d.ts.map +1 -1
- package/dist/_internal/platform/auth/index.js +2 -0
- package/dist/_internal/platform/auth/ios-keychain-token-store.d.ts +88 -0
- package/dist/_internal/platform/auth/ios-keychain-token-store.d.ts.map +1 -0
- package/dist/_internal/platform/auth/ios-keychain-token-store.js +147 -0
- package/dist/_internal/platform/auth/session-manager.d.ts +2 -0
- package/dist/_internal/platform/auth/session-manager.d.ts.map +1 -1
- package/dist/_internal/platform/auth/session-manager.js +9 -1
- package/dist/_internal/platform/auth/token-store.d.ts +13 -0
- package/dist/_internal/platform/auth/token-store.d.ts.map +1 -1
- package/dist/_internal/platform/auth/token-store.js +23 -0
- package/dist/_internal/platform/companion/companion-chat-manager.d.ts +64 -11
- package/dist/_internal/platform/companion/companion-chat-manager.d.ts.map +1 -1
- package/dist/_internal/platform/companion/companion-chat-manager.js +158 -12
- package/dist/_internal/platform/companion/companion-chat-persistence.d.ts +33 -0
- package/dist/_internal/platform/companion/companion-chat-persistence.d.ts.map +1 -0
- package/dist/_internal/platform/companion/companion-chat-persistence.js +115 -0
- package/dist/_internal/platform/companion/companion-chat-rate-limiter.d.ts +47 -0
- package/dist/_internal/platform/companion/companion-chat-rate-limiter.d.ts.map +1 -0
- package/dist/_internal/platform/companion/companion-chat-rate-limiter.js +117 -0
- package/dist/_internal/platform/companion/companion-chat-types.d.ts +2 -4
- package/dist/_internal/platform/companion/companion-chat-types.d.ts.map +1 -1
- package/dist/_internal/platform/companion/companion-chat-types.js +2 -4
- package/dist/_internal/platform/companion/index.d.ts +4 -0
- package/dist/_internal/platform/companion/index.d.ts.map +1 -1
- package/dist/_internal/platform/companion/index.js +2 -0
- package/dist/_internal/platform/daemon/facade-composition.d.ts.map +1 -1
- package/dist/_internal/platform/daemon/facade-composition.js +5 -0
- package/dist/_internal/platform/daemon/facade.d.ts.map +1 -1
- package/dist/_internal/platform/daemon/facade.js +3 -0
- package/dist/_internal/platform/daemon/http/runtime-route-types.d.ts +0 -7
- package/dist/_internal/platform/daemon/http/runtime-route-types.d.ts.map +1 -1
- package/dist/_internal/platform/state/db.d.ts.map +1 -1
- package/dist/_internal/platform/state/db.js +0 -1
- package/dist/_internal/platform/state/sqlite-store.d.ts.map +1 -1
- package/dist/_internal/platform/state/sqlite-store.js +0 -1
- package/dist/_internal/platform/version.js +1 -1
- package/dist/_internal/transport-core/client-transport.d.ts.map +1 -1
- package/dist/_internal/transport-core/event-envelope.d.ts.map +1 -1
- package/dist/_internal/transport-core/event-feeds.d.ts.map +1 -1
- package/dist/_internal/transport-core/index.d.ts +5 -0
- package/dist/_internal/transport-core/index.d.ts.map +1 -1
- package/dist/_internal/transport-core/index.js +3 -0
- package/dist/_internal/transport-core/middleware.d.ts +76 -0
- package/dist/_internal/transport-core/middleware.d.ts.map +1 -0
- package/dist/_internal/transport-core/middleware.js +67 -0
- package/dist/_internal/transport-core/observer.d.ts +53 -0
- package/dist/_internal/transport-core/observer.d.ts.map +1 -0
- package/dist/_internal/transport-core/observer.js +26 -0
- package/dist/_internal/transport-core/otel.d.ts +64 -0
- package/dist/_internal/transport-core/otel.d.ts.map +1 -0
- package/dist/_internal/transport-core/otel.js +149 -0
- package/dist/_internal/transport-direct/index.d.ts.map +1 -1
- package/dist/_internal/transport-direct/index.js +0 -1
- package/dist/_internal/transport-http/contract-client.d.ts +11 -1
- package/dist/_internal/transport-http/contract-client.d.ts.map +1 -1
- package/dist/_internal/transport-http/contract-client.js +18 -4
- package/dist/_internal/transport-http/http-core.d.ts +27 -1
- package/dist/_internal/transport-http/http-core.d.ts.map +1 -1
- package/dist/_internal/transport-http/http-core.js +180 -12
- package/dist/_internal/transport-http/http.d.ts +3 -3
- package/dist/_internal/transport-http/http.d.ts.map +1 -1
- package/dist/_internal/transport-http/http.js +2 -2
- package/dist/_internal/transport-http/index.d.ts +4 -2
- package/dist/_internal/transport-http/index.d.ts.map +1 -1
- package/dist/_internal/transport-http/index.js +2 -1
- package/dist/_internal/transport-http/paths.js +1 -1
- package/dist/_internal/transport-http/reconnect.d.ts +2 -0
- package/dist/_internal/transport-http/reconnect.d.ts.map +1 -1
- package/dist/_internal/transport-http/reconnect.js +4 -2
- package/dist/_internal/transport-http/retry.d.ts +15 -0
- package/dist/_internal/transport-http/retry.d.ts.map +1 -1
- package/dist/_internal/transport-http/retry.js +19 -0
- package/dist/_internal/transport-realtime/domain-events.d.ts.map +1 -1
- package/dist/_internal/transport-realtime/runtime-events.d.ts +10 -3
- package/dist/_internal/transport-realtime/runtime-events.d.ts.map +1 -1
- package/dist/_internal/transport-realtime/runtime-events.js +73 -8
- package/dist/auth.d.ts +38 -3
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +68 -3
- package/dist/client.d.ts +61 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +64 -3
- package/dist/expo.d.ts +1 -0
- package/dist/expo.d.ts.map +1 -1
- package/dist/expo.js +1 -0
- package/dist/observer/index.d.ts +16 -25
- package/dist/observer/index.d.ts.map +1 -1
- package/dist/platform/runtime/transports/http.js +1 -1
- package/dist/react-native.d.ts +2 -0
- package/dist/react-native.d.ts.map +1 -1
- package/dist/react-native.js +2 -0
- package/package.json +16 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"android-keystore-token-store.d.ts","sourceRoot":"","sources":["../../../../src/_internal/platform/auth/android-keystore-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAM5D;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B,cAAc,GACd,iCAAiC,GACjC,sBAAsB,GACtB,yCAAyC,GACzC,iBAAiB,GACjB,sBAAsB,CAAC;AAE3B;;GAEG;AACH,MAAM,MAAM,iBAAiB,GACzB,eAAe,GACf,oBAAoB,GACpB,QAAQ,GACR,oCAAoC,GACpC,gCAAgC,GAChC,qCAAqC,GACrC,yBAAyB,CAAC;AAM9B,MAAM,WAAW,gCAAgC;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,oBAAoB,CAAC;IAE9C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,iBAAiB,CAAC;CACzC;AAkDD,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACpE;;;;OAIG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvE;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxE;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,gCAAqC,EAC9C,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GACpC,yBAAyB,CAuF3B"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* android-keystore-token-store.ts
|
|
3
|
+
*
|
|
4
|
+
* Token store backed by `react-native-keychain` for bare React Native on Android.
|
|
5
|
+
*
|
|
6
|
+
* On Android, `react-native-keychain` routes to EncryptedSharedPreferences
|
|
7
|
+
* backed by the Android Keystore system — hardware-backed AES-256-GCM
|
|
8
|
+
* encryption where available (API 23+). This is the **recommended**
|
|
9
|
+
* implementation for proper hardware-backed security.
|
|
10
|
+
*
|
|
11
|
+
* Alternative: `@react-native-async-storage/async-storage` can store the
|
|
12
|
+
* token but is NOT hardware-backed. If you cannot use `react-native-keychain`,
|
|
13
|
+
* wrap async-storage with an additional encryption layer (e.g.
|
|
14
|
+
* `react-native-encrypted-storage`). The `react-native-keychain` path is
|
|
15
|
+
* strongly preferred for production apps.
|
|
16
|
+
*
|
|
17
|
+
* `react-native-keychain` is an **optional peer dependency** — this module
|
|
18
|
+
* does NOT import it at the top level.
|
|
19
|
+
*
|
|
20
|
+
* ## Installation
|
|
21
|
+
*
|
|
22
|
+
* ```sh
|
|
23
|
+
* npm install react-native-keychain
|
|
24
|
+
* # Android: auto-linked via Gradle; no manual step required
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Wave 6 three-part error messages: [what happened] · [why] · [what to do]
|
|
28
|
+
*/
|
|
29
|
+
import { GoodVibesSdkError } from '../../errors/index.js';
|
|
30
|
+
let _mod = null;
|
|
31
|
+
async function loadKeychain() {
|
|
32
|
+
if (_mod !== null)
|
|
33
|
+
return _mod;
|
|
34
|
+
try {
|
|
35
|
+
_mod = await import('react-native-keychain');
|
|
36
|
+
return _mod;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new GoodVibesSdkError('react-native-keychain is not installed — the Android Keystore token store cannot be initialised. ' +
|
|
40
|
+
'This optional peer dependency is required to persist tokens in Android Keystore-backed storage. ' +
|
|
41
|
+
'Run `npm install react-native-keychain` and rebuild your app.', {
|
|
42
|
+
code: 'RN_KEYCHAIN_NOT_INSTALLED',
|
|
43
|
+
category: 'config',
|
|
44
|
+
source: 'config',
|
|
45
|
+
recoverable: false,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Fixed username slot
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
const USERNAME_SLOT = 'goodvibes-sdk';
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Factory
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
/**
|
|
57
|
+
* Create a `GoodVibesTokenStore` backed by the Android Keystore system via
|
|
58
|
+
* `react-native-keychain`.
|
|
59
|
+
*
|
|
60
|
+
* On Android, `react-native-keychain` uses `EncryptedSharedPreferences` backed
|
|
61
|
+
* by the Android Keystore — hardware-backed AES-256-GCM encryption where
|
|
62
|
+
* supported (API 23+). This is strongly preferred over plain AsyncStorage.
|
|
63
|
+
*
|
|
64
|
+
* Pass `accessControl: 'BIOMETRY_ANY'` or `'DEVICE_PASSCODE'` to require
|
|
65
|
+
* interactive user authentication before reading the stored credential.
|
|
66
|
+
*
|
|
67
|
+
* `react-native-keychain` is an **optional peer dependency** — install it with:
|
|
68
|
+
*
|
|
69
|
+
* ```sh
|
|
70
|
+
* npm install react-native-keychain
|
|
71
|
+
* # No manual Gradle/CocoaPods step required for Android
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { createAndroidKeystoreTokenStore, createReactNativeGoodVibesSdk } from '@pellux/goodvibes-sdk/react-native';
|
|
77
|
+
*
|
|
78
|
+
* const tokenStore = createAndroidKeystoreTokenStore({
|
|
79
|
+
* service: 'com.myapp.gv',
|
|
80
|
+
* accessControl: 'BIOMETRY_ANY',
|
|
81
|
+
* });
|
|
82
|
+
* const sdk = createReactNativeGoodVibesSdk({ baseUrl: 'https://daemon.example.com', tokenStore });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function createAndroidKeystoreTokenStore(options = {}, __loadModule) {
|
|
86
|
+
const service = options.service ?? 'com.pellux.goodvibes-sdk';
|
|
87
|
+
const accessible = options.accessible ?? 'WHEN_UNLOCKED_THIS_DEVICE_ONLY';
|
|
88
|
+
const accessControl = options.accessControl;
|
|
89
|
+
async function resolveModule() {
|
|
90
|
+
if (__loadModule !== undefined) {
|
|
91
|
+
return __loadModule();
|
|
92
|
+
}
|
|
93
|
+
return loadKeychain();
|
|
94
|
+
}
|
|
95
|
+
function buildOptions(mod) {
|
|
96
|
+
const opts = { service };
|
|
97
|
+
const accessibleValue = mod.ACCESSIBLE[accessible];
|
|
98
|
+
if (accessibleValue !== undefined) {
|
|
99
|
+
opts['accessible'] = accessibleValue;
|
|
100
|
+
}
|
|
101
|
+
else if (options.accessible !== undefined) {
|
|
102
|
+
console.warn(`[pellux/goodvibes-sdk] react-native-keychain does not expose ACCESSIBLE.${accessible}; falling back to default`);
|
|
103
|
+
}
|
|
104
|
+
if (accessControl !== undefined) {
|
|
105
|
+
const acValue = mod.ACCESS_CONTROL[accessControl];
|
|
106
|
+
if (acValue !== undefined) {
|
|
107
|
+
opts['accessControl'] = acValue;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.warn(`[pellux/goodvibes-sdk] react-native-keychain does not expose ACCESS_CONTROL.${accessControl}; falling back to default`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return opts;
|
|
114
|
+
}
|
|
115
|
+
async function readPayload() {
|
|
116
|
+
const mod = await resolveModule();
|
|
117
|
+
const result = await mod.getGenericPassword(buildOptions(mod));
|
|
118
|
+
if (result === false)
|
|
119
|
+
return null;
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(result.password);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function writePayload(payload) {
|
|
128
|
+
const mod = await resolveModule();
|
|
129
|
+
if (payload === null) {
|
|
130
|
+
await mod.resetGenericPassword(buildOptions(mod));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await mod.setGenericPassword(USERNAME_SLOT, JSON.stringify(payload), buildOptions(mod));
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
async getToken() {
|
|
137
|
+
const payload = await readPayload();
|
|
138
|
+
return payload?.token ?? null;
|
|
139
|
+
},
|
|
140
|
+
async setToken(token) {
|
|
141
|
+
if (token === null) {
|
|
142
|
+
await writePayload(null);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await writePayload({ token, expiresAt: null });
|
|
146
|
+
},
|
|
147
|
+
async clearToken() {
|
|
148
|
+
await writePayload(null);
|
|
149
|
+
},
|
|
150
|
+
async setTokenEntry(token, expiresAt) {
|
|
151
|
+
if (token === null) {
|
|
152
|
+
await writePayload(null);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await writePayload({ token, expiresAt: expiresAt ?? null });
|
|
156
|
+
},
|
|
157
|
+
async getTokenEntry() {
|
|
158
|
+
const payload = await readPayload();
|
|
159
|
+
if (payload === null)
|
|
160
|
+
return { token: null };
|
|
161
|
+
return { token: payload.token, expiresAt: payload.expiresAt ?? undefined };
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-refresh transport middleware.
|
|
3
|
+
*
|
|
4
|
+
* Integrates `AutoRefreshCoordinator` at the transport boundary so that ALL
|
|
5
|
+
* typed operator/peer SDK calls benefit from silent token refresh — not only
|
|
6
|
+
* `auth.current()`.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* 1. Pre-flight: call `coordinator.ensureFreshToken()` before dispatching the
|
|
10
|
+
* request. Updates `ctx.headers.Authorization` with the fresh token so
|
|
11
|
+
* consumer middleware (which runs after this one) sees the current token.
|
|
12
|
+
* 2. Reactive 401: if `next()` throws a 401-shaped error, trigger a refresh
|
|
13
|
+
* via `coordinator.refreshAndRetryOnce()` and set `ctx.response` to the
|
|
14
|
+
* retry result so the transport can return it to the caller.
|
|
15
|
+
* 3. Loop prevention: retry requests carry `__gv_ar_attempted: true` in their
|
|
16
|
+
* options. The middleware recognises this flag and passes through without
|
|
17
|
+
* additional refresh logic, preventing infinite recursion.
|
|
18
|
+
* 4. Error passthrough: terminal auth errors from the coordinator are placed
|
|
19
|
+
* directly onto `ctx.error` rather than thrown, so the transport's
|
|
20
|
+
* middleware-error-wrapping instrumentation does not re-wrap them as
|
|
21
|
+
* `kind:'unknown'`.
|
|
22
|
+
*/
|
|
23
|
+
import type { TransportMiddleware } from '../../transport-core/index.js';
|
|
24
|
+
import type { HttpJsonTransport } from '../../transport-http/http-core.js';
|
|
25
|
+
import type { GoodVibesTokenStore } from '../../../auth.js';
|
|
26
|
+
import type { AutoRefreshCoordinator } from './auto-refresh.js';
|
|
27
|
+
/**
|
|
28
|
+
* Create a transport middleware that integrates the `AutoRefreshCoordinator`
|
|
29
|
+
* into every HTTP request.
|
|
30
|
+
*
|
|
31
|
+
* @param coordinator - The auto-refresh coordinator managing token lifecycle.
|
|
32
|
+
* @param transport - The HTTP JSON transport used to re-issue the request on
|
|
33
|
+
* reactive 401 retry. Must be the same transport instance
|
|
34
|
+
* whose middleware chain contains this middleware, so that
|
|
35
|
+
* the retry benefits from other middleware (e.g. logging,
|
|
36
|
+
* tracing) except for another auto-refresh cycle.
|
|
37
|
+
* @param tokenStore - The token store from which to read the fresh token after
|
|
38
|
+
* pre-flight refresh, so `ctx.headers.Authorization` can
|
|
39
|
+
* be updated before consumer middleware runs.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const mw = createAutoRefreshMiddleware(coordinator, transport, tokenStore);
|
|
43
|
+
* transport.use(mw);
|
|
44
|
+
*/
|
|
45
|
+
export declare function createAutoRefreshMiddleware(coordinator: AutoRefreshCoordinator, transport: Pick<HttpJsonTransport, 'requestJson'>, tokenStore: GoodVibesTokenStore): TransportMiddleware;
|
|
46
|
+
//# sourceMappingURL=auto-refresh-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-refresh-middleware.d.ts","sourceRoot":"","sources":["../../../../src/_internal/platform/auth/auto-refresh-middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAoB,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAC3F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAKhE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,sBAAsB,EACnC,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,aAAa,CAAC,EACjD,UAAU,EAAE,mBAAmB,GAC9B,mBAAmB,CAyGrB"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-refresh transport middleware.
|
|
3
|
+
*
|
|
4
|
+
* Integrates `AutoRefreshCoordinator` at the transport boundary so that ALL
|
|
5
|
+
* typed operator/peer SDK calls benefit from silent token refresh — not only
|
|
6
|
+
* `auth.current()`.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* 1. Pre-flight: call `coordinator.ensureFreshToken()` before dispatching the
|
|
10
|
+
* request. Updates `ctx.headers.Authorization` with the fresh token so
|
|
11
|
+
* consumer middleware (which runs after this one) sees the current token.
|
|
12
|
+
* 2. Reactive 401: if `next()` throws a 401-shaped error, trigger a refresh
|
|
13
|
+
* via `coordinator.refreshAndRetryOnce()` and set `ctx.response` to the
|
|
14
|
+
* retry result so the transport can return it to the caller.
|
|
15
|
+
* 3. Loop prevention: retry requests carry `__gv_ar_attempted: true` in their
|
|
16
|
+
* options. The middleware recognises this flag and passes through without
|
|
17
|
+
* additional refresh logic, preventing infinite recursion.
|
|
18
|
+
* 4. Error passthrough: terminal auth errors from the coordinator are placed
|
|
19
|
+
* directly onto `ctx.error` rather than thrown, so the transport's
|
|
20
|
+
* middleware-error-wrapping instrumentation does not re-wrap them as
|
|
21
|
+
* `kind:'unknown'`.
|
|
22
|
+
*/
|
|
23
|
+
/** Internal flag key — never conflicts with public HttpJsonRequestOptions fields. */
|
|
24
|
+
const ATTEMPTED_FLAG = '__gv_ar_attempted';
|
|
25
|
+
/**
|
|
26
|
+
* Create a transport middleware that integrates the `AutoRefreshCoordinator`
|
|
27
|
+
* into every HTTP request.
|
|
28
|
+
*
|
|
29
|
+
* @param coordinator - The auto-refresh coordinator managing token lifecycle.
|
|
30
|
+
* @param transport - The HTTP JSON transport used to re-issue the request on
|
|
31
|
+
* reactive 401 retry. Must be the same transport instance
|
|
32
|
+
* whose middleware chain contains this middleware, so that
|
|
33
|
+
* the retry benefits from other middleware (e.g. logging,
|
|
34
|
+
* tracing) except for another auto-refresh cycle.
|
|
35
|
+
* @param tokenStore - The token store from which to read the fresh token after
|
|
36
|
+
* pre-flight refresh, so `ctx.headers.Authorization` can
|
|
37
|
+
* be updated before consumer middleware runs.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const mw = createAutoRefreshMiddleware(coordinator, transport, tokenStore);
|
|
41
|
+
* transport.use(mw);
|
|
42
|
+
*/
|
|
43
|
+
export function createAutoRefreshMiddleware(coordinator, transport, tokenStore) {
|
|
44
|
+
return async function autoRefreshMiddleware(ctx, next) {
|
|
45
|
+
// ── Loop-prevention guard ────────────────────────────────────────────────
|
|
46
|
+
// Retry requests issued by this middleware carry the flag. When we see it,
|
|
47
|
+
// simply forward the request without any refresh logic.
|
|
48
|
+
// We catch and re-place errors onto ctx.error so they are NOT tagged as
|
|
49
|
+
// "middleware errors" by the transport's instrumented-chain wrapper — which
|
|
50
|
+
// would re-wrap them as GoodVibesSdkError{kind:'unknown'} and hide the
|
|
51
|
+
// original 401 status that refreshAndRetryOnce needs to detect.
|
|
52
|
+
if (ctx.options[ATTEMPTED_FLAG] === true) {
|
|
53
|
+
try {
|
|
54
|
+
await next();
|
|
55
|
+
}
|
|
56
|
+
catch (passthroughErr) {
|
|
57
|
+
ctx.error = passthroughErr;
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// ── Pre-flight refresh ───────────────────────────────────────────────────
|
|
62
|
+
// Silently refreshes the token when it is within the leeway window.
|
|
63
|
+
// After refresh, update ctx.headers.Authorization with the fresh token so
|
|
64
|
+
// consumer middleware (which runs after this one in the chain) sees the
|
|
65
|
+
// current Bearer token, not the stale one that was set when ctx was built.
|
|
66
|
+
await coordinator.ensureFreshToken();
|
|
67
|
+
const freshToken = await tokenStore.getToken();
|
|
68
|
+
if (freshToken) {
|
|
69
|
+
ctx.headers['Authorization'] = `Bearer ${freshToken}`;
|
|
70
|
+
}
|
|
71
|
+
// ── Dispatch request ─────────────────────────────────────────────────────
|
|
72
|
+
// We catch ALL errors from next() rather than re-throwing them directly.
|
|
73
|
+
// Re-throwing from within a middleware causes the transport's instrumented-
|
|
74
|
+
// chain wrapper to tag the error as "from this middleware" and re-wrap it
|
|
75
|
+
// as GoodVibesSdkError{kind:'unknown'}, even when the error originated
|
|
76
|
+
// from the real fetch (innerFetch). By placing non-401 errors onto
|
|
77
|
+
// ctx.error instead, they bypass the middleware-wrapping path and propagate
|
|
78
|
+
// with their original type and kind intact.
|
|
79
|
+
let caughtErr;
|
|
80
|
+
let nextSucceeded = false;
|
|
81
|
+
try {
|
|
82
|
+
await next();
|
|
83
|
+
nextSucceeded = true;
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
caughtErr = err;
|
|
87
|
+
}
|
|
88
|
+
if (nextSucceeded) {
|
|
89
|
+
return; // success — nothing more to do
|
|
90
|
+
}
|
|
91
|
+
if (!is401Error(caughtErr)) {
|
|
92
|
+
// Non-401 error (e.g. 5xx, network) — place on ctx.error so it exits
|
|
93
|
+
// the middleware chain without triggering middleware-error wrapping.
|
|
94
|
+
ctx.error = caughtErr;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// ── Reactive 401 retry ─────────────────────────────────────────────────
|
|
98
|
+
// Build retry options that preserve the original request attributes and
|
|
99
|
+
// carry the loop-prevention flag so the next pass through this middleware
|
|
100
|
+
// just calls next() without re-entering the refresh logic.
|
|
101
|
+
const retryOptions = {
|
|
102
|
+
method: ctx.method,
|
|
103
|
+
body: ctx.body,
|
|
104
|
+
signal: ctx.signal,
|
|
105
|
+
[ATTEMPTED_FLAG]: true,
|
|
106
|
+
};
|
|
107
|
+
// Delegate to coordinator.refreshAndRetryOnce — this refreshes the token
|
|
108
|
+
// exactly once and executes the retry fn once. If the retry also returns
|
|
109
|
+
// 401, it throws GoodVibesSdkError{kind:'auth'} (Wave 6 three-part message).
|
|
110
|
+
//
|
|
111
|
+
// IMPORTANT: we place terminal errors onto ctx.error instead of re-throwing
|
|
112
|
+
// them. This bypasses the transport's middleware-error-wrapping path
|
|
113
|
+
// (which would re-label them as kind:'unknown') and lets the transport's
|
|
114
|
+
// outer `if (ctx.error) throw ctx.error` propagate them cleanly.
|
|
115
|
+
let retryResult;
|
|
116
|
+
try {
|
|
117
|
+
retryResult = await coordinator.refreshAndRetryOnce(async () => {
|
|
118
|
+
// transport.requestJson goes through the full middleware chain; the
|
|
119
|
+
// ATTEMPTED_FLAG ensures this middleware is a passthrough on that call.
|
|
120
|
+
return transport.requestJson(ctx.url, retryOptions);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (retryErr) {
|
|
124
|
+
// Place onto ctx.error — transport's post-chain check will rethrow it
|
|
125
|
+
// without the middleware-wrapping treatment.
|
|
126
|
+
ctx.error = retryErr;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Put the retry result back onto ctx.response so the transport's outer
|
|
130
|
+
// requestJson can resolve it via `await ctx.response.json()`.
|
|
131
|
+
// Also clear ctx.error: the innerFetch path sets it when it throws (before
|
|
132
|
+
// re-throwing), so it reflects the original 401. Since we handled the 401
|
|
133
|
+
// successfully, we must clear it so the transport's `if (ctx.error) throw`
|
|
134
|
+
// guard does not re-raise the already-resolved error.
|
|
135
|
+
ctx.error = undefined;
|
|
136
|
+
ctx.response = new Response(JSON.stringify(retryResult), {
|
|
137
|
+
status: 200,
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Helpers
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
/** Detect a 401 response across the various error shapes the SDK produces. */
|
|
146
|
+
function is401Error(error) {
|
|
147
|
+
if (typeof error !== 'object' || error === null)
|
|
148
|
+
return false;
|
|
149
|
+
const e = error;
|
|
150
|
+
const status = e.status ??
|
|
151
|
+
e.transport?.status ??
|
|
152
|
+
e.response?.status ??
|
|
153
|
+
e.cause?.response?.status;
|
|
154
|
+
return status === 401;
|
|
155
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutoRefreshCoordinator — Silent token refresh with in-flight request queuing.
|
|
3
|
+
*
|
|
4
|
+
* Prevents user-visible 401s by:
|
|
5
|
+
* 1. Pre-flight leeway check: if the token expires within refreshLeewayMs,
|
|
6
|
+
* trigger a silent refresh before the request is dispatched.
|
|
7
|
+
* 2. Reactive 401 retry: if a request returns 401 and the token wasn't
|
|
8
|
+
* already known expired, trigger a refresh then retry the request once.
|
|
9
|
+
* 3. In-flight queuing: while a refresh is in progress, subsequent refresh
|
|
10
|
+
* attempts queue on the same promise — one refresh call for all waiters.
|
|
11
|
+
*
|
|
12
|
+
* When no refresh endpoint is available (the coordinator has no `refresh`
|
|
13
|
+
* function), the pre-flight check is a graceful no-op and the reactive path
|
|
14
|
+
* triggers a token re-read (which may succeed if the store was updated externally).
|
|
15
|
+
*
|
|
16
|
+
* Wave 6 three-part error messages:
|
|
17
|
+
* [what happened] · [why] · [what to do]
|
|
18
|
+
*/
|
|
19
|
+
import type { GoodVibesTokenStore } from '../../../auth.js';
|
|
20
|
+
import type { SDKObserver } from '../../../observer/index.js';
|
|
21
|
+
export interface AutoRefreshOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Enable or disable silent token refresh. Default: `true`.
|
|
24
|
+
* When `false`, 401 responses propagate immediately without retry.
|
|
25
|
+
*/
|
|
26
|
+
readonly autoRefresh?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Milliseconds before token expiry to trigger a silent refresh.
|
|
29
|
+
* Default: 60_000 (1 minute).
|
|
30
|
+
*/
|
|
31
|
+
readonly refreshLeewayMs?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Consumer-provided callback invoked to obtain a new token when the current
|
|
34
|
+
* token is near expiry (pre-flight leeway check) or a 401 is received
|
|
35
|
+
* (reactive retry path).
|
|
36
|
+
*
|
|
37
|
+
* The callback must return the new token string and, optionally, its expiry
|
|
38
|
+
* timestamp in Unix milliseconds. When provided, the coordinator calls this
|
|
39
|
+
* to perform the actual token refresh and persists the result via
|
|
40
|
+
* `setTokenEntry` (or `setToken` on stores that don't implement
|
|
41
|
+
* `setTokenEntry`).
|
|
42
|
+
*
|
|
43
|
+
* When absent, the coordinator's pre-flight check is a graceful no-op and
|
|
44
|
+
* the reactive 401 path re-reads the token store without making a network
|
|
45
|
+
* call (useful when an external party updates the store).
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const store = createMemoryTokenStore(initialToken);
|
|
49
|
+
* const sdk = createGoodVibesSdk({
|
|
50
|
+
* baseUrl: 'https://daemon.example.com',
|
|
51
|
+
* tokenStore: store,
|
|
52
|
+
* autoRefresh: {
|
|
53
|
+
* refresh: async () => {
|
|
54
|
+
* const res = await fetch('/api/auth/refresh', { method: 'POST' });
|
|
55
|
+
* const { token, expiresAt } = await res.json();
|
|
56
|
+
* return { token, expiresAt };
|
|
57
|
+
* },
|
|
58
|
+
* },
|
|
59
|
+
* });
|
|
60
|
+
*/
|
|
61
|
+
readonly refresh?: () => Promise<{
|
|
62
|
+
token: string;
|
|
63
|
+
expiresAt?: number;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
export interface AutoRefreshCoordinatorOptions {
|
|
67
|
+
readonly tokenStore: GoodVibesTokenStore;
|
|
68
|
+
readonly autoRefresh: boolean;
|
|
69
|
+
readonly refreshLeewayMs: number;
|
|
70
|
+
/**
|
|
71
|
+
* Optional refresh function. Called to acquire a new token when the current
|
|
72
|
+
* one is near expiry or a 401 was received.
|
|
73
|
+
*
|
|
74
|
+
* If undefined, the coordinator performs a graceful no-op (does not
|
|
75
|
+
* error) — in-flight queuing and leeway checks are still respected, but
|
|
76
|
+
* no network call is made. Reactive 401 retry still re-reads the token
|
|
77
|
+
* store in case an external party updated it.
|
|
78
|
+
*/
|
|
79
|
+
readonly refresh?: () => Promise<{
|
|
80
|
+
token: string;
|
|
81
|
+
expiresAt?: number;
|
|
82
|
+
}>;
|
|
83
|
+
readonly observer?: SDKObserver;
|
|
84
|
+
}
|
|
85
|
+
export declare class AutoRefreshCoordinator {
|
|
86
|
+
#private;
|
|
87
|
+
constructor(options: AutoRefreshCoordinatorOptions);
|
|
88
|
+
/**
|
|
89
|
+
* Call before dispatching a request. If the token is near expiry (within
|
|
90
|
+
* `refreshLeewayMs`), silently refreshes before the request goes out.
|
|
91
|
+
*
|
|
92
|
+
* If `autoRefresh` is disabled, this is a no-op.
|
|
93
|
+
*/
|
|
94
|
+
ensureFreshToken(): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Execute `fn` and retry once on 401 after triggering a refresh.
|
|
97
|
+
*
|
|
98
|
+
* If `autoRefresh` is false, the first 401 is rethrown immediately.
|
|
99
|
+
* If the retry also returns 401, throws `GoodVibesSdkError{kind:'auth'}`.
|
|
100
|
+
*
|
|
101
|
+
* @param fn - The request function to execute. Must be side-effect-safe to
|
|
102
|
+
* call twice (called at most twice).
|
|
103
|
+
*/
|
|
104
|
+
withRetryOn401<T>(fn: () => Promise<T>): Promise<T>;
|
|
105
|
+
/**
|
|
106
|
+
* Refresh the token immediately and execute `fn` exactly once as the retry.
|
|
107
|
+
*
|
|
108
|
+
* Unlike `withRetryOn401`, this method does NOT call `fn` before refreshing —
|
|
109
|
+
* it assumes the caller already received a 401 on the initial attempt. It
|
|
110
|
+
* refreshes the token (serialised via the shared promise, as with
|
|
111
|
+
* `withRetryOn401`) and then calls `fn` a single time.
|
|
112
|
+
*
|
|
113
|
+
* If `fn` throws a 401 on retry, a terminal `GoodVibesSdkError{kind:'auth'}`
|
|
114
|
+
* is thrown with the Wave 6 three-part message format.
|
|
115
|
+
*
|
|
116
|
+
* Used by `createAutoRefreshMiddleware` to avoid making an extra HTTP call
|
|
117
|
+
* when the middleware already observed the initial 401 from `next()`.
|
|
118
|
+
*
|
|
119
|
+
* @param fn - The retry request to execute after the refresh completes.
|
|
120
|
+
*/
|
|
121
|
+
refreshAndRetryOnce<T>(fn: () => Promise<T>): Promise<T>;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=auto-refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-refresh.d.ts","sourceRoot":"","sources":["../../../../src/_internal/platform/auth/auto-refresh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAO9D,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAE/B;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAElC;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzE;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC;CACjC;AAMD,qBAAa,sBAAsB;;gBAUrB,OAAO,EAAE,6BAA6B;IAsGlD;;;;;OAKG;IACG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAYvC;;;;;;;;OAQG;IACG,cAAc,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IA0CzD;;;;;;;;;;;;;;;OAeG;IACG,mBAAmB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CA2B/D"}
|