@oxyhq/core 1.11.14 → 1.11.16
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/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/mixins/OxyServices.appData.js +108 -0
- package/dist/cjs/mixins/OxyServices.popup.js +42 -2
- package/dist/cjs/mixins/index.js +2 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/mixins/OxyServices.appData.js +103 -0
- package/dist/esm/mixins/OxyServices.popup.js +42 -2
- package/dist/esm/mixins/index.js +2 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mixins/OxyServices.appData.d.ts +107 -0
- package/dist/types/mixins/index.d.ts +2 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/mixins/OxyServices.appData.ts +158 -0
- package/src/mixins/OxyServices.popup.ts +47 -2
- package/src/mixins/__tests__/appData.test.ts +203 -0
- package/src/mixins/index.ts +3 -0
package/dist/esm/index.js
CHANGED
|
@@ -22,6 +22,7 @@ export { OXY_CLOUD_URL, oxyClient } from './OxyServices.js';
|
|
|
22
22
|
export { AuthManager, createAuthManager } from './AuthManager.js';
|
|
23
23
|
export { CrossDomainAuth, createCrossDomainAuth } from './CrossDomainAuth.js';
|
|
24
24
|
export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth.js';
|
|
25
|
+
export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData.js';
|
|
25
26
|
// --- Crypto / Identity ---
|
|
26
27
|
export { KeyManager, SignatureService, RecoveryPhraseService, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto/index.js';
|
|
27
28
|
// --- Models & Types ---
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-user App-Data Mixin
|
|
3
|
+
*
|
|
4
|
+
* Thin client around `/users/me/app-data/...` — a generic per-user JSON KV
|
|
5
|
+
* store on the API. Authenticated callers can read, write, list, and delete
|
|
6
|
+
* entries scoped to their own user account.
|
|
7
|
+
*
|
|
8
|
+
* Identifier rules (must match the API):
|
|
9
|
+
* - Both `namespace` and `key` must match `/^[a-z0-9_-]{1,64}$/u`.
|
|
10
|
+
*
|
|
11
|
+
* Limits (enforced by the API):
|
|
12
|
+
* - Serialized JSON values are capped at 64 KB.
|
|
13
|
+
* - Writes are rate-limited to 100 / minute / user.
|
|
14
|
+
*
|
|
15
|
+
* Intended use cases are small bits of cross-device app state — e.g. Academy
|
|
16
|
+
* course progress, "last viewed" markers, dismissed banner flags. Do not use
|
|
17
|
+
* this for large blobs or anything that needs server-side querying; it's a
|
|
18
|
+
* write-it-and-read-it-back store.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Identifier validator — mirror of the API regex. Validating client-side
|
|
22
|
+
* gives consumers a clean throw before the request even leaves the device.
|
|
23
|
+
*/
|
|
24
|
+
const APP_DATA_IDENTIFIER_PATTERN = /^[a-z0-9_-]{1,64}$/u;
|
|
25
|
+
/** Thrown when a namespace or key fails the kebab/snake-case validator. */
|
|
26
|
+
export class OxyAppDataIdentifierError extends Error {
|
|
27
|
+
constructor(field, value) {
|
|
28
|
+
super(`Invalid app-data ${field} "${value}": must match [a-z0-9_-]{1,64} (lowercase letters, digits, dashes, underscores).`);
|
|
29
|
+
this.name = 'OxyAppDataIdentifierError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function assertIdentifier(field, value) {
|
|
33
|
+
if (!APP_DATA_IDENTIFIER_PATTERN.test(value)) {
|
|
34
|
+
throw new OxyAppDataIdentifierError(field, value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function OxyServicesAppDataMixin(Base) {
|
|
38
|
+
return class extends Base {
|
|
39
|
+
constructor(...args) {
|
|
40
|
+
super(...args);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Read the value stored under `(namespace, key)` for the current user.
|
|
44
|
+
*
|
|
45
|
+
* @returns The previously-stored value, or `null` if nothing has been
|
|
46
|
+
* stored yet. Never throws on "not found" — a missing entry is
|
|
47
|
+
* semantically a `null` value.
|
|
48
|
+
*/
|
|
49
|
+
async getAppData(namespace, key) {
|
|
50
|
+
assertIdentifier('namespace', namespace);
|
|
51
|
+
assertIdentifier('key', key);
|
|
52
|
+
return this.withAuthRetry(async () => {
|
|
53
|
+
const response = await this.makeRequest('GET', `/users/me/app-data/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, undefined, { cache: false });
|
|
54
|
+
return response?.value ?? null;
|
|
55
|
+
}, 'getAppData');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Upsert the value under `(namespace, key)` for the current user.
|
|
59
|
+
*
|
|
60
|
+
* Returns the value the server confirmed it stored — typically the same
|
|
61
|
+
* value the caller passed in, but consumers should prefer the returned
|
|
62
|
+
* value (the API is the source of truth).
|
|
63
|
+
*
|
|
64
|
+
* @throws OxyAppDataIdentifierError when namespace or key is malformed.
|
|
65
|
+
*/
|
|
66
|
+
async setAppData(namespace, key, value) {
|
|
67
|
+
assertIdentifier('namespace', namespace);
|
|
68
|
+
assertIdentifier('key', key);
|
|
69
|
+
return this.withAuthRetry(async () => {
|
|
70
|
+
const response = await this.makeRequest('PUT', `/users/me/app-data/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, { value }, { cache: false });
|
|
71
|
+
// The server echoes the stored value back; fall back to the caller's
|
|
72
|
+
// input only if the server somehow omitted it (defensive — the route
|
|
73
|
+
// always sets it).
|
|
74
|
+
return (response?.value ?? value);
|
|
75
|
+
}, 'setAppData');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Delete the value stored under `(namespace, key)` for the current user.
|
|
79
|
+
*
|
|
80
|
+
* Idempotent — resolves successfully whether or not the entry existed.
|
|
81
|
+
*/
|
|
82
|
+
async deleteAppData(namespace, key) {
|
|
83
|
+
assertIdentifier('namespace', namespace);
|
|
84
|
+
assertIdentifier('key', key);
|
|
85
|
+
await this.withAuthRetry(async () => {
|
|
86
|
+
await this.makeRequest('DELETE', `/users/me/app-data/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`, undefined, { cache: false });
|
|
87
|
+
}, 'deleteAppData');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* List every entry stored under `namespace` for the current user.
|
|
91
|
+
*
|
|
92
|
+
* Returns an empty object when the namespace contains no entries (the
|
|
93
|
+
* endpoint never 404s on an empty namespace).
|
|
94
|
+
*/
|
|
95
|
+
async listAppData(namespace) {
|
|
96
|
+
assertIdentifier('namespace', namespace);
|
|
97
|
+
return this.withAuthRetry(async () => {
|
|
98
|
+
const response = await this.makeRequest('GET', `/users/me/app-data/${encodeURIComponent(namespace)}`, undefined, { cache: false });
|
|
99
|
+
return response?.entries ?? {};
|
|
100
|
+
}, 'listAppData');
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -168,8 +168,48 @@ export function OxyServicesPopupAuthMixin(Base) {
|
|
|
168
168
|
document.body.appendChild(iframe);
|
|
169
169
|
try {
|
|
170
170
|
const session = await this.waitForIframeAuth(iframe, timeout, clientId);
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
// Bail early on incomplete responses. The iframe contract requires
|
|
172
|
+
// both an access token and a session id; anything less is unusable.
|
|
173
|
+
// Returning `null` here (without installing the token) prevents a
|
|
174
|
+
// stale credential from being committed to HttpService when the
|
|
175
|
+
// user is actually signed out — that pattern caused a `getCurrentUser`
|
|
176
|
+
// -> 401 -> token-clear loop in consumer apps because callers gated
|
|
177
|
+
// on `session?.user` and never installed the user via
|
|
178
|
+
// `handleAuthSuccess`, while HttpService quietly held the token.
|
|
179
|
+
const accessToken = session ? session.accessToken : undefined;
|
|
180
|
+
if (!session || !accessToken || !session.sessionId) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
// Snapshot the previous token so we can roll back if the user
|
|
184
|
+
// lookup below fails — this avoids leaving a half-committed session
|
|
185
|
+
// (token installed, user missing) which would let the next
|
|
186
|
+
// authenticated request 401 with no way to recover.
|
|
187
|
+
const previousAccessToken = this.httpService.getAccessToken();
|
|
188
|
+
this.httpService.setTokens(accessToken);
|
|
189
|
+
// The iframe typically returns `{ sessionId, accessToken }` without
|
|
190
|
+
// user data. Fetch the user explicitly so callers receive a
|
|
191
|
+
// fully-formed session and never need a second `/users/me` round
|
|
192
|
+
// trip. If this fails the session is unusable — revert the token
|
|
193
|
+
// and return null so the caller treats this exactly like a
|
|
194
|
+
// missing-session response.
|
|
195
|
+
if (!session.user) {
|
|
196
|
+
try {
|
|
197
|
+
const userData = await this.makeRequest('GET', `/session/user/${session.sessionId}`, undefined, { cache: false, retry: false });
|
|
198
|
+
if (!userData) {
|
|
199
|
+
throw new Error('Empty user response');
|
|
200
|
+
}
|
|
201
|
+
session.user = userData;
|
|
202
|
+
}
|
|
203
|
+
catch (userError) {
|
|
204
|
+
debug.warn('silentSignIn: failed to fetch user data, rolling back token', userError);
|
|
205
|
+
if (previousAccessToken) {
|
|
206
|
+
this.httpService.setTokens(previousAccessToken);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
this.httpService.clearTokens();
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
173
213
|
}
|
|
174
214
|
return session;
|
|
175
215
|
}
|
package/dist/esm/mixins/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { OxyServicesFeaturesMixin } from './OxyServices.features.js';
|
|
|
25
25
|
import { OxyServicesTopicsMixin } from './OxyServices.topics.js';
|
|
26
26
|
import { OxyServicesManagedAccountsMixin } from './OxyServices.managedAccounts.js';
|
|
27
27
|
import { OxyServicesContactsMixin } from './OxyServices.contacts.js';
|
|
28
|
+
import { OxyServicesAppDataMixin } from './OxyServices.appData.js';
|
|
28
29
|
/**
|
|
29
30
|
* Mixin pipeline - applied in order from first to last.
|
|
30
31
|
*
|
|
@@ -64,6 +65,7 @@ const MIXIN_PIPELINE = [
|
|
|
64
65
|
OxyServicesTopicsMixin,
|
|
65
66
|
OxyServicesManagedAccountsMixin,
|
|
66
67
|
OxyServicesContactsMixin,
|
|
68
|
+
OxyServicesAppDataMixin,
|
|
67
69
|
// Utility (last, can use all above)
|
|
68
70
|
OxyServicesUtilityMixin,
|
|
69
71
|
];
|