@freeappstore/sdk 0.1.0 → 0.1.2
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/auth.d.ts +15 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +32 -6
- package/dist/auth.js.map +1 -1
- package/dist/auth.test.d.ts +2 -0
- package/dist/auth.test.d.ts.map +1 -0
- package/dist/auth.test.js +149 -0
- package/dist/auth.test.js.map +1 -0
- package/dist/kv.d.ts +0 -8
- package/dist/kv.d.ts.map +1 -1
- package/dist/kv.js +16 -0
- package/dist/kv.js.map +1 -1
- package/dist/kv.test.d.ts +2 -0
- package/dist/kv.test.d.ts.map +1 -0
- package/dist/kv.test.js +68 -0
- package/dist/kv.test.js.map +1 -0
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -11,12 +11,25 @@ export declare class Auth {
|
|
|
11
11
|
/**
|
|
12
12
|
* Redirect-based GitHub OAuth. Opens the platform's hosted OAuth start URL,
|
|
13
13
|
* which redirects back to the current page with a session token in the hash.
|
|
14
|
+
*
|
|
15
|
+
* The current page's `location.hash` is dropped from `return_to` because
|
|
16
|
+
* the OAuth callback writes its own `#fas_session=…` and would clobber any
|
|
17
|
+
* hash-based router state otherwise.
|
|
14
18
|
*/
|
|
15
19
|
signIn(): void;
|
|
16
20
|
signOut(): void;
|
|
17
21
|
/**
|
|
18
|
-
* Call this once at app start
|
|
19
|
-
*
|
|
22
|
+
* Call this once at app start, before rendering anything that depends on
|
|
23
|
+
* auth state. If the page was loaded via an auth callback (e.g. after
|
|
24
|
+
* `signIn()` returned from GitHub), this captures the session from the
|
|
25
|
+
* URL hash, persists it to localStorage, and clears the hash. On a normal
|
|
26
|
+
* page load it's a no-op — the constructor already restored any cached
|
|
27
|
+
* session from localStorage.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const fas = initApp({ appId: 'my-app' });
|
|
31
|
+
* await fas.auth.init();
|
|
32
|
+
* render();
|
|
20
33
|
*/
|
|
21
34
|
init(): Promise<void>;
|
|
22
35
|
private fetchUser;
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AASpD,qBAAa,IAAI;IAKb,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,SAAS,CAA0C;gBAGxC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM;IAKlC,IAAI,IAAI,IAAI,IAAI,GAAG,IAAI,CAEtB;IAED,IAAI,KAAK,IAAI,MAAM,GAAG,IAAI,CAEzB;IAED,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,GAAG,WAAW;IAK5D
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AASpD,qBAAa,IAAI;IAKb,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,SAAS,CAA0C;gBAGxC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM;IAKlC,IAAI,IAAI,IAAI,IAAI,GAAG,IAAI,CAEtB;IAED,IAAI,KAAK,IAAI,MAAM,GAAG,IAAI,CAEzB;IAED,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,GAAG,WAAW;IAK5D;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;IASd,OAAO,IAAI,IAAI;IAMf;;;;;;;;;;;;OAYG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAyBb,SAAS;IAQvB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,IAAI;CAGb"}
|
package/dist/auth.js
CHANGED
|
@@ -22,12 +22,17 @@ export class Auth {
|
|
|
22
22
|
/**
|
|
23
23
|
* Redirect-based GitHub OAuth. Opens the platform's hosted OAuth start URL,
|
|
24
24
|
* which redirects back to the current page with a session token in the hash.
|
|
25
|
+
*
|
|
26
|
+
* The current page's `location.hash` is dropped from `return_to` because
|
|
27
|
+
* the OAuth callback writes its own `#fas_session=…` and would clobber any
|
|
28
|
+
* hash-based router state otherwise.
|
|
25
29
|
*/
|
|
26
30
|
signIn() {
|
|
27
|
-
const
|
|
31
|
+
const here = new URL(window.location.href);
|
|
32
|
+
here.hash = '';
|
|
28
33
|
const url = new URL('/v1/auth/github/start', this.apiBase);
|
|
29
34
|
url.searchParams.set('app_id', this.appId);
|
|
30
|
-
url.searchParams.set('return_to',
|
|
35
|
+
url.searchParams.set('return_to', here.toString());
|
|
31
36
|
window.location.assign(url.toString());
|
|
32
37
|
}
|
|
33
38
|
signOut() {
|
|
@@ -36,8 +41,17 @@ export class Auth {
|
|
|
36
41
|
this.emit();
|
|
37
42
|
}
|
|
38
43
|
/**
|
|
39
|
-
* Call this once at app start
|
|
40
|
-
*
|
|
44
|
+
* Call this once at app start, before rendering anything that depends on
|
|
45
|
+
* auth state. If the page was loaded via an auth callback (e.g. after
|
|
46
|
+
* `signIn()` returned from GitHub), this captures the session from the
|
|
47
|
+
* URL hash, persists it to localStorage, and clears the hash. On a normal
|
|
48
|
+
* page load it's a no-op — the constructor already restored any cached
|
|
49
|
+
* session from localStorage.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const fas = initApp({ appId: 'my-app' });
|
|
53
|
+
* await fas.auth.init();
|
|
54
|
+
* render();
|
|
41
55
|
*/
|
|
42
56
|
async init() {
|
|
43
57
|
if (typeof window === 'undefined')
|
|
@@ -45,11 +59,23 @@ export class Auth {
|
|
|
45
59
|
const hash = window.location.hash;
|
|
46
60
|
if (!hash.startsWith('#fas_session='))
|
|
47
61
|
return;
|
|
48
|
-
|
|
62
|
+
// Always clear the hash before doing anything else — even on failure.
|
|
63
|
+
// Otherwise a bad token gets re-tried on every reload and the user is
|
|
64
|
+
// permanently stuck on a "broken" URL.
|
|
65
|
+
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
66
|
+
let token;
|
|
67
|
+
try {
|
|
68
|
+
token = decodeURIComponent(hash.slice('#fas_session='.length));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Malformed hash (% with nothing after, etc.). Hash already cleared.
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!token)
|
|
75
|
+
return;
|
|
49
76
|
const user = await this.fetchUser(token);
|
|
50
77
|
this.session = { token, user };
|
|
51
78
|
this.writeStorage(this.session);
|
|
52
|
-
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
53
79
|
this.emit();
|
|
54
80
|
}
|
|
55
81
|
async fetchUser(token) {
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,GAAG,aAAa,CAAC;AAOlC,MAAM,OAAO,IAAI;IAKI;IACA;IALX,OAAO,GAAmB,IAAI,CAAC;IAC/B,SAAS,GAAG,IAAI,GAAG,EAA+B,CAAC;IAE3D,YACmB,KAAa,EACb,OAAe;QADf,UAAK,GAAL,KAAK,CAAQ;QACb,YAAO,GAAP,OAAO,CAAQ;QAEhC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACpC,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,IAAI,CAAC;IACpC,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC;IACrC,CAAC;IAED,QAAQ,CAAC,QAAqC;QAC5C,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC;IAED
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.test.d.ts","sourceRoot":"","sources":["../src/auth.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { Auth } from './auth.js';
|
|
3
|
+
function makeWindow(initial) {
|
|
4
|
+
const url = new URL(initial.href);
|
|
5
|
+
if (initial.hash)
|
|
6
|
+
url.hash = initial.hash;
|
|
7
|
+
const assigns = [];
|
|
8
|
+
const replaces = [];
|
|
9
|
+
const storage = {
|
|
10
|
+
store: new Map(),
|
|
11
|
+
getItem: (k) => storage.store.get(k) ?? null,
|
|
12
|
+
setItem: (k, v) => void storage.store.set(k, v),
|
|
13
|
+
removeItem: (k) => void storage.store.delete(k),
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
window: {
|
|
17
|
+
location: {
|
|
18
|
+
href: url.toString(),
|
|
19
|
+
hash: url.hash,
|
|
20
|
+
pathname: url.pathname,
|
|
21
|
+
search: url.search,
|
|
22
|
+
assign: (u) => assigns.push(u),
|
|
23
|
+
},
|
|
24
|
+
localStorage: storage,
|
|
25
|
+
history: {
|
|
26
|
+
replaceState: (_s, _t, u) => replaces.push(u),
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
assigns,
|
|
30
|
+
replaces,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
// happy-dom-style globals; we install per-test
|
|
35
|
+
delete globalThis['window'];
|
|
36
|
+
delete globalThis['history'];
|
|
37
|
+
delete globalThis['fetch'];
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
delete globalThis['window'];
|
|
41
|
+
delete globalThis['history'];
|
|
42
|
+
delete globalThis['fetch'];
|
|
43
|
+
});
|
|
44
|
+
describe('Auth.signIn', () => {
|
|
45
|
+
it('strips the hash from return_to so the OAuth callback does not clobber app router state', () => {
|
|
46
|
+
const { window, assigns } = makeWindow({
|
|
47
|
+
href: 'https://app.freeappstore.online/dashboard?team=42#/settings/profile',
|
|
48
|
+
});
|
|
49
|
+
globalThis['window'] = window;
|
|
50
|
+
const auth = new Auth('demo', 'https://api.freeappstore.online');
|
|
51
|
+
auth.signIn();
|
|
52
|
+
expect(assigns).toHaveLength(1);
|
|
53
|
+
const target = new URL(assigns[0]);
|
|
54
|
+
const returnTo = target.searchParams.get('return_to');
|
|
55
|
+
expect(returnTo).toBe('https://app.freeappstore.online/dashboard?team=42');
|
|
56
|
+
// No #/settings/profile carried into return_to.
|
|
57
|
+
expect(returnTo).not.toContain('#');
|
|
58
|
+
});
|
|
59
|
+
it('still preserves pathname + querystring', () => {
|
|
60
|
+
const { window, assigns } = makeWindow({ href: 'https://app.example/page?x=1' });
|
|
61
|
+
globalThis['window'] = window;
|
|
62
|
+
const auth = new Auth('demo', 'https://api.example');
|
|
63
|
+
auth.signIn();
|
|
64
|
+
const returnTo = new URL(assigns[0]).searchParams.get('return_to');
|
|
65
|
+
expect(returnTo).toBe('https://app.example/page?x=1');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('Auth.init — hash handling', () => {
|
|
69
|
+
it('clears the hash even when the auth fetch fails (regression: no stuck state)', async () => {
|
|
70
|
+
const { window, replaces } = makeWindow({
|
|
71
|
+
href: 'https://app.example/',
|
|
72
|
+
hash: '#fas_session=bad-token',
|
|
73
|
+
});
|
|
74
|
+
globalThis['window'] = window;
|
|
75
|
+
globalThis['history'] = window.history;
|
|
76
|
+
globalThis.fetch = vi.fn().mockResolvedValue(new Response('', { status: 401 }));
|
|
77
|
+
const auth = new Auth('demo', 'https://api.example');
|
|
78
|
+
await expect(auth.init()).rejects.toThrow();
|
|
79
|
+
// The hash was cleared before the fetch, so reload won't retry.
|
|
80
|
+
expect(replaces).toHaveLength(1);
|
|
81
|
+
expect(replaces[0]).toBe('/');
|
|
82
|
+
expect(auth.user).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
it('captures session and clears hash on successful auth callback', async () => {
|
|
85
|
+
const { window, replaces } = makeWindow({
|
|
86
|
+
href: 'https://app.example/dashboard',
|
|
87
|
+
hash: '#fas_session=good-token',
|
|
88
|
+
});
|
|
89
|
+
globalThis['window'] = window;
|
|
90
|
+
globalThis['history'] = window.history;
|
|
91
|
+
globalThis.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ id: 'gh:1', login: 'alice', avatarUrl: null }), {
|
|
92
|
+
status: 200,
|
|
93
|
+
}));
|
|
94
|
+
const auth = new Auth('demo', 'https://api.example');
|
|
95
|
+
await auth.init();
|
|
96
|
+
expect(auth.user?.login).toBe('alice');
|
|
97
|
+
expect(auth.token).toBe('good-token');
|
|
98
|
+
expect(window.localStorage.store.get('fas:session')).toContain('good-token');
|
|
99
|
+
expect(replaces[0]).toBe('/dashboard');
|
|
100
|
+
});
|
|
101
|
+
it('is a no-op when no fas_session hash is present', async () => {
|
|
102
|
+
const { window, replaces } = makeWindow({ href: 'https://app.example/' });
|
|
103
|
+
globalThis['window'] = window;
|
|
104
|
+
globalThis['history'] = window.history;
|
|
105
|
+
const fetchSpy = vi.fn();
|
|
106
|
+
globalThis.fetch = fetchSpy;
|
|
107
|
+
const auth = new Auth('demo', 'https://api.example');
|
|
108
|
+
await auth.init();
|
|
109
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
110
|
+
expect(replaces).toHaveLength(0);
|
|
111
|
+
expect(auth.user).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('Auth — storage round-trip', () => {
|
|
115
|
+
it('reads cached session from localStorage in the constructor', () => {
|
|
116
|
+
const { window } = makeWindow({ href: 'https://app.example/' });
|
|
117
|
+
window.localStorage.store.set('fas:session', JSON.stringify({
|
|
118
|
+
token: 'cached',
|
|
119
|
+
user: { id: 'gh:7', login: 'returning-user', avatarUrl: null },
|
|
120
|
+
}));
|
|
121
|
+
globalThis['window'] = window;
|
|
122
|
+
const auth = new Auth('demo', 'https://api.example');
|
|
123
|
+
expect(auth.user?.login).toBe('returning-user');
|
|
124
|
+
expect(auth.token).toBe('cached');
|
|
125
|
+
});
|
|
126
|
+
it('signOut clears storage and notifies listeners', () => {
|
|
127
|
+
const { window } = makeWindow({ href: 'https://app.example/' });
|
|
128
|
+
window.localStorage.store.set('fas:session', JSON.stringify({
|
|
129
|
+
token: 'cached',
|
|
130
|
+
user: { id: 'gh:7', login: 'u', avatarUrl: null },
|
|
131
|
+
}));
|
|
132
|
+
globalThis['window'] = window;
|
|
133
|
+
const auth = new Auth('demo', 'https://api.example');
|
|
134
|
+
const seen = [];
|
|
135
|
+
auth.onChange((u) => seen.push(u));
|
|
136
|
+
auth.signOut();
|
|
137
|
+
expect(auth.user).toBeNull();
|
|
138
|
+
expect(window.localStorage.store.has('fas:session')).toBe(false);
|
|
139
|
+
expect(seen).toEqual([null]);
|
|
140
|
+
});
|
|
141
|
+
it('returns null when localStorage has corrupt JSON (does not crash)', () => {
|
|
142
|
+
const { window } = makeWindow({ href: 'https://app.example/' });
|
|
143
|
+
window.localStorage.store.set('fas:session', '{ not: json');
|
|
144
|
+
globalThis['window'] = window;
|
|
145
|
+
const auth = new Auth('demo', 'https://api.example');
|
|
146
|
+
expect(auth.user).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
//# sourceMappingURL=auth.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.test.js","sourceRoot":"","sources":["../src/auth.test.ts"],"names":[],"mappings":"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"}
|
package/dist/kv.d.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import type { Auth } from './auth.js';
|
|
2
|
-
/**
|
|
3
|
-
* Per-user key-value store, scoped to (appId, userId).
|
|
4
|
-
*
|
|
5
|
-
* Limits (enforced server-side):
|
|
6
|
-
* - max 1MB total per user
|
|
7
|
-
* - max 100 keys per user
|
|
8
|
-
* - max 64KB per value
|
|
9
|
-
*/
|
|
10
2
|
export declare class Kv {
|
|
11
3
|
private readonly appId;
|
|
12
4
|
private readonly apiBase;
|
package/dist/kv.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kv.d.ts","sourceRoot":"","sources":["../src/kv.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"kv.d.ts","sourceRoot":"","sources":["../src/kv.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAyBtC,qBAAa,EAAE;IAEX,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAFJ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,IAAI;IAGvB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAQhD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IActD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxC,OAAO,CAAC,OAAO;CAahB"}
|
package/dist/kv.js
CHANGED
|
@@ -5,7 +5,20 @@
|
|
|
5
5
|
* - max 1MB total per user
|
|
6
6
|
* - max 100 keys per user
|
|
7
7
|
* - max 64KB per value
|
|
8
|
+
*
|
|
9
|
+
* Keys are non-empty strings ≤ 128 chars. We validate client-side so the
|
|
10
|
+
* server doesn't have to deal with edge-case URLs like `/kv/` or absurdly
|
|
11
|
+
* long path segments.
|
|
8
12
|
*/
|
|
13
|
+
const MAX_KEY_LENGTH = 128;
|
|
14
|
+
function assertValidKey(key) {
|
|
15
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
16
|
+
throw new Error('kv key must be a non-empty string.');
|
|
17
|
+
}
|
|
18
|
+
if (key.length > MAX_KEY_LENGTH) {
|
|
19
|
+
throw new Error(`kv key exceeds ${MAX_KEY_LENGTH} chars.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
9
22
|
export class Kv {
|
|
10
23
|
appId;
|
|
11
24
|
apiBase;
|
|
@@ -16,6 +29,7 @@ export class Kv {
|
|
|
16
29
|
this.auth = auth;
|
|
17
30
|
}
|
|
18
31
|
async get(key) {
|
|
32
|
+
assertValidKey(key);
|
|
19
33
|
const res = await this.request('GET', key);
|
|
20
34
|
if (res.status === 404)
|
|
21
35
|
return null;
|
|
@@ -24,6 +38,7 @@ export class Kv {
|
|
|
24
38
|
return (await res.json());
|
|
25
39
|
}
|
|
26
40
|
async set(key, value) {
|
|
41
|
+
assertValidKey(key);
|
|
27
42
|
// JSON.stringify(undefined) returns undefined, which would store an empty
|
|
28
43
|
// body and break later get() calls. Reject up front instead.
|
|
29
44
|
if (value === undefined) {
|
|
@@ -36,6 +51,7 @@ export class Kv {
|
|
|
36
51
|
}
|
|
37
52
|
}
|
|
38
53
|
async delete(key) {
|
|
54
|
+
assertValidKey(key);
|
|
39
55
|
const res = await this.request('DELETE', key);
|
|
40
56
|
if (!res.ok && res.status !== 404) {
|
|
41
57
|
throw new Error(`kv.delete failed: ${res.status}`);
|
package/dist/kv.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kv.js","sourceRoot":"","sources":["../src/kv.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"kv.js","sourceRoot":"","sources":["../src/kv.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kv.test.d.ts","sourceRoot":"","sources":["../src/kv.test.ts"],"names":[],"mappings":""}
|
package/dist/kv.test.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { Kv } from './kv.js';
|
|
3
|
+
function fakeAuth(token) {
|
|
4
|
+
return { token };
|
|
5
|
+
}
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// 204/304 require a null body per WHATWG; an empty string would throw.
|
|
8
|
+
globalThis.fetch = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
delete globalThis['fetch'];
|
|
12
|
+
});
|
|
13
|
+
describe('Kv key validation', () => {
|
|
14
|
+
const kv = (token = 'tok') => new Kv('demo', 'https://api.example', fakeAuth(token));
|
|
15
|
+
it('rejects empty-string key (regression: would yield /kv/ URL and a confusing 404)', async () => {
|
|
16
|
+
await expect(kv().get('')).rejects.toThrow(/non-empty/);
|
|
17
|
+
await expect(kv().set('', 1)).rejects.toThrow(/non-empty/);
|
|
18
|
+
await expect(kv().delete('')).rejects.toThrow(/non-empty/);
|
|
19
|
+
});
|
|
20
|
+
it('rejects keys longer than 128 chars', async () => {
|
|
21
|
+
const long = 'x'.repeat(129);
|
|
22
|
+
await expect(kv().set(long, 1)).rejects.toThrow(/128/);
|
|
23
|
+
});
|
|
24
|
+
it('accepts a 128-char key (boundary)', async () => {
|
|
25
|
+
const ok = 'x'.repeat(128);
|
|
26
|
+
await expect(kv().set(ok, 1)).resolves.toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
it('accepts ordinary keys', async () => {
|
|
29
|
+
await expect(kv().set('foo', 1)).resolves.toBeUndefined();
|
|
30
|
+
await expect(kv().set('a/b', 1)).resolves.toBeUndefined();
|
|
31
|
+
await expect(kv().set('color-preference', 1)).resolves.toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('Kv.set value validation', () => {
|
|
35
|
+
const kv = () => new Kv('demo', 'https://api.example', fakeAuth('tok'));
|
|
36
|
+
it('rejects undefined (regression: would store empty body, then break get)', async () => {
|
|
37
|
+
await expect(kv().set('k', undefined)).rejects.toThrow(/undefined/);
|
|
38
|
+
});
|
|
39
|
+
it('accepts null as a stored value', async () => {
|
|
40
|
+
await expect(kv().set('k', null)).resolves.toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('Kv auth gate', () => {
|
|
44
|
+
it('throws Not signed in when no session token', async () => {
|
|
45
|
+
const k = new Kv('demo', 'https://api.example', fakeAuth(null));
|
|
46
|
+
await expect(k.get('foo')).rejects.toThrow(/not signed in/i);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('Kv.get behavior', () => {
|
|
50
|
+
it('returns null on 404, the missing-key signal', async () => {
|
|
51
|
+
globalThis.fetch = vi.fn().mockResolvedValue(new Response('', { status: 404 }));
|
|
52
|
+
const k = new Kv('demo', 'https://api.example', fakeAuth('tok'));
|
|
53
|
+
expect(await k.get('missing')).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
it('parses JSON on 200', async () => {
|
|
56
|
+
globalThis.fetch = vi
|
|
57
|
+
.fn()
|
|
58
|
+
.mockResolvedValue(new Response(JSON.stringify({ x: 1 }), { status: 200 }));
|
|
59
|
+
const k = new Kv('demo', 'https://api.example', fakeAuth('tok'));
|
|
60
|
+
expect(await k.get('foo')).toEqual({ x: 1 });
|
|
61
|
+
});
|
|
62
|
+
it('throws on non-404 errors', async () => {
|
|
63
|
+
globalThis.fetch = vi.fn().mockResolvedValue(new Response('', { status: 500 }));
|
|
64
|
+
const k = new Kv('demo', 'https://api.example', fakeAuth('tok'));
|
|
65
|
+
await expect(k.get('foo')).rejects.toThrow(/500/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=kv.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kv.test.js","sourceRoot":"","sources":["../src/kv.test.ts"],"names":[],"mappings":"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"}
|