@atlasauth/chrome-extension 0.1.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/README.md +90 -0
- package/dist/background.d.ts +22 -0
- package/dist/background.js +39 -0
- package/dist/background.js.map +1 -0
- package/dist/client.d.ts +102 -0
- package/dist/client.js +168 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +33 -0
- package/dist/react.js +30 -0
- package/dist/react.js.map +1 -0
- package/dist/storage.d.ts +79 -0
- package/dist/storage.js +107 -0
- package/dist/storage.js.map +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @atlasauth/chrome-extension
|
|
2
|
+
|
|
3
|
+
The official Atlas SDK for Chrome / MV3 browser extensions — the extension peer
|
|
4
|
+
of [`@atlasauth/react`](../react). It reuses the framework-agnostic
|
|
5
|
+
[`@atlasauth/js`](../js) client for every API call and, through the `/react`
|
|
6
|
+
entry point, the full React hook + component surface.
|
|
7
|
+
|
|
8
|
+
The one architectural difference from the web SDK: **the web SDK carries the
|
|
9
|
+
session in a cookie; a browser extension has no cookie jar to the Atlas FAPI
|
|
10
|
+
origin.** So the session token is cached in `chrome.storage.local` and attached
|
|
11
|
+
to every request by hand, as a bearer.
|
|
12
|
+
|
|
13
|
+
## Entry points
|
|
14
|
+
|
|
15
|
+
| Import from | Pulls in React? | Use in |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `@atlasauth/chrome-extension` | no | anywhere (core: store + client) |
|
|
18
|
+
| `@atlasauth/chrome-extension/background` | no | the service worker |
|
|
19
|
+
| `@atlasauth/chrome-extension/react` | yes (peer) | popup / options / side panel |
|
|
20
|
+
|
|
21
|
+
## Background service worker
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createAtlasClient } from '@atlasauth/chrome-extension/background';
|
|
25
|
+
|
|
26
|
+
const atlas = createAtlasClient({
|
|
27
|
+
publishableKey: 'pk_...',
|
|
28
|
+
frontendApi: 'https://fapi.your-instance.atlasauth.net',
|
|
29
|
+
background: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// The service worker has no long-lived timer (MV3 evicts it), so refresh the
|
|
33
|
+
// token whenever it wakes instead of on an in-page interval.
|
|
34
|
+
chrome.alarms.create('atlas-refresh', { periodInMinutes: 1 });
|
|
35
|
+
chrome.alarms.onAlarm.addListener((a) => {
|
|
36
|
+
if (a.name === 'atlas-refresh') void atlas.syncSessionToken();
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`syncSessionToken()` reads the stored token, and if it is inside its refresh lead
|
|
41
|
+
window (or you pass `{ force: true }`) mints a fresh one via
|
|
42
|
+
`POST /v1/client/sessions/:sid/tokens` and writes it back to `chrome.storage`.
|
|
43
|
+
|
|
44
|
+
## Popup / options page (React)
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { AtlasProvider, useAuth, useUser } from '@atlasauth/chrome-extension/react';
|
|
48
|
+
|
|
49
|
+
export function App() {
|
|
50
|
+
return (
|
|
51
|
+
<AtlasProvider
|
|
52
|
+
publishableKey="pk_..."
|
|
53
|
+
frontendApi="https://fapi.your-instance.atlasauth.net"
|
|
54
|
+
>
|
|
55
|
+
<Profile />
|
|
56
|
+
</AtlasProvider>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The provider wraps `@atlasauth/react`'s provider and injects a `fetch` that
|
|
62
|
+
attaches the stored token as a bearer and captures any refreshed token back into
|
|
63
|
+
`chrome.storage`. The token must already be in the store — seed it with
|
|
64
|
+
`client.setToken(jwt)` after a hosted sign-in.
|
|
65
|
+
|
|
66
|
+
## The store
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { StorageCache } from '@atlasauth/chrome-extension';
|
|
70
|
+
|
|
71
|
+
const store = new StorageCache(); // chrome.storage.local, default key
|
|
72
|
+
await store.setToken(jwt);
|
|
73
|
+
await store.getToken(); // string | null
|
|
74
|
+
await store.removeToken();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`chrome` is typed structurally, so the package needs no `@types/chrome`. Pass a
|
|
78
|
+
custom `area` (e.g. `chrome.storage.session`, or `createMemoryStorageArea()` in
|
|
79
|
+
tests) to change where the token lives.
|
|
80
|
+
|
|
81
|
+
## Manifest
|
|
82
|
+
|
|
83
|
+
Add the `storage` permission and the Atlas FAPI origin as a host permission:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"permissions": ["storage", "alarms"],
|
|
88
|
+
"host_permissions": ["https://fapi.your-instance.atlasauth.net/*"]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@atlasauth/chrome-extension/background` — the service-worker surface.
|
|
3
|
+
*
|
|
4
|
+
* A convenience alias for the React-free core, named for where it is used. A
|
|
5
|
+
* background service worker creates the client once and refreshes the token on
|
|
6
|
+
* each wake:
|
|
7
|
+
*
|
|
8
|
+
* import { createAtlasClient } from '@atlasauth/chrome-extension/background';
|
|
9
|
+
*
|
|
10
|
+
* const atlas = createAtlasClient({
|
|
11
|
+
* publishableKey: 'pk_...',
|
|
12
|
+
* frontendApi: 'https://fapi.your-instance.atlasauth.net',
|
|
13
|
+
* background: true,
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* chrome.alarms.create('atlas-refresh', { periodInMinutes: 1 });
|
|
17
|
+
* chrome.alarms.onAlarm.addListener((a) => {
|
|
18
|
+
* if (a.name === 'atlas-refresh') void atlas.syncSessionToken();
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
21
|
+
export * from './storage';
|
|
22
|
+
export * from './client';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* `@atlasauth/chrome-extension/background` — the service-worker surface.
|
|
19
|
+
*
|
|
20
|
+
* A convenience alias for the React-free core, named for where it is used. A
|
|
21
|
+
* background service worker creates the client once and refreshes the token on
|
|
22
|
+
* each wake:
|
|
23
|
+
*
|
|
24
|
+
* import { createAtlasClient } from '@atlasauth/chrome-extension/background';
|
|
25
|
+
*
|
|
26
|
+
* const atlas = createAtlasClient({
|
|
27
|
+
* publishableKey: 'pk_...',
|
|
28
|
+
* frontendApi: 'https://fapi.your-instance.atlasauth.net',
|
|
29
|
+
* background: true,
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* chrome.alarms.create('atlas-refresh', { periodInMinutes: 1 });
|
|
33
|
+
* chrome.alarms.onAlarm.addListener((a) => {
|
|
34
|
+
* if (a.name === 'atlas-refresh') void atlas.syncSessionToken();
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
__exportStar(require("./storage"), exports);
|
|
38
|
+
__exportStar(require("./client"), exports);
|
|
39
|
+
//# sourceMappingURL=background.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background.js","sourceRoot":"","sources":["../src/background.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,4CAA0B;AAC1B,2CAAyB"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { FapiClient, type ProviderToken } from '@atlasauth/js';
|
|
2
|
+
import { type TokenStore } from './storage';
|
|
3
|
+
/**
|
|
4
|
+
* The extension FAPI layer.
|
|
5
|
+
*
|
|
6
|
+
* Every network call goes through the framework-agnostic `@atlasauth/js`
|
|
7
|
+
* `FapiClient` — the same client the web and React Native SDKs use — so there is
|
|
8
|
+
* one implementation of "how an Atlas request is shaped". This module adds the
|
|
9
|
+
* one thing an extension needs that a first-party web page gets for free:
|
|
10
|
+
* **bearer auth**. The browser attaches the session via a cookie; an extension
|
|
11
|
+
* has none, so the token from the {@link TokenStore} rides in an
|
|
12
|
+
* `Authorization: Bearer` header instead.
|
|
13
|
+
*/
|
|
14
|
+
/** The session claims the SDK reads off the (unverified) JWT — a scheduling hint. */
|
|
15
|
+
interface SessionTokenClaims {
|
|
16
|
+
sid?: string;
|
|
17
|
+
exp?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Read the unverified claims out of a JWT. Never a security decision — the
|
|
20
|
+
* server validates the signature; this only tells the client which session to
|
|
21
|
+
* refresh and roughly when. Returns null for anything that is not a JWT. */
|
|
22
|
+
export declare function decodeTokenClaims(jwt: string): SessionTokenClaims | null;
|
|
23
|
+
/**
|
|
24
|
+
* Wrap a `fetch` so every request carries the current session token as a bearer.
|
|
25
|
+
* The token is read FRESH from the store on each call — never captured — so a
|
|
26
|
+
* refresh or a sign-out between requests is always reflected. A request made
|
|
27
|
+
* while signed out simply carries no `Authorization` header.
|
|
28
|
+
*/
|
|
29
|
+
export declare function bearerFetch(fetchImpl: typeof fetch, store: TokenStore): typeof fetch;
|
|
30
|
+
/**
|
|
31
|
+
* A {@link bearerFetch} that ALSO captures a freshly-minted session token out of
|
|
32
|
+
* the response and persists it to the store.
|
|
33
|
+
*
|
|
34
|
+
* The web SDK's provider keeps its refreshed JWT in memory, which is exactly
|
|
35
|
+
* what an extension cannot do — the popup that received the token is closed
|
|
36
|
+
* moments later, and the service worker is torn down. So when a token-issuing
|
|
37
|
+
* endpoint (`/v1/client/...`) answers with a `jwt` in its JSON body, we write it
|
|
38
|
+
* back to `chrome.storage`, so the next request from ANY surface — and the next
|
|
39
|
+
* cold start of the service worker — picks up the current token. Scoped to
|
|
40
|
+
* `/v1/client` responses and guarded by JSON parsing, so a non-JSON or unrelated
|
|
41
|
+
* response is left untouched.
|
|
42
|
+
*/
|
|
43
|
+
export declare function persistingBearerFetch(fetchImpl: typeof fetch, store: TokenStore): typeof fetch;
|
|
44
|
+
export interface CreateAtlasClientOptions {
|
|
45
|
+
publishableKey: string;
|
|
46
|
+
/** The instance's FAPI origin, e.g. `https://fapi.your-instance.atlasauth.net`. */
|
|
47
|
+
frontendApi: string;
|
|
48
|
+
/** Where the token lives. Defaults to `chrome.storage.local` via {@link StorageCache}. */
|
|
49
|
+
store?: TokenStore;
|
|
50
|
+
/** Injectable for tests; defaults to the platform `fetch`. */
|
|
51
|
+
fetchImpl?: typeof fetch;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The extension client. A thin, React-free handle the background service worker
|
|
55
|
+
* and any extension page share: the `@atlasauth/js` FAPI client (bearer-wired),
|
|
56
|
+
* the token store, and the session-token lifecycle helpers.
|
|
57
|
+
*/
|
|
58
|
+
export interface AtlasClient {
|
|
59
|
+
/** The bearer-wired `@atlasauth/js` client for any raw FAPI call. */
|
|
60
|
+
readonly fapi: FapiClient;
|
|
61
|
+
/** The token store this client reads and writes. */
|
|
62
|
+
readonly store: TokenStore;
|
|
63
|
+
/** The current session token, or null when signed out. */
|
|
64
|
+
getToken(): Promise<string | null>;
|
|
65
|
+
/** Seed the token after an out-of-band sign-in (e.g. a hosted-page flow). */
|
|
66
|
+
setToken(token: string): Promise<void>;
|
|
67
|
+
/** The signed-in user's live token at a connected provider (Google, …). */
|
|
68
|
+
getProviderToken(provider: string): Promise<ProviderToken | null>;
|
|
69
|
+
/** Refresh the token if it is near expiry (or `force`) and persist it. */
|
|
70
|
+
syncSessionToken(options?: SyncOptions): Promise<string | null>;
|
|
71
|
+
/** Revoke the session server-side and clear the stored token. */
|
|
72
|
+
signOut(): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build an {@link AtlasClient}. `background: true` is accepted for parity with
|
|
76
|
+
* the sibling SDKs and to read as intent at the call site; the client behaves
|
|
77
|
+
* the same in a service worker as on a page.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createAtlasClient(options: CreateAtlasClientOptions & {
|
|
80
|
+
background?: boolean;
|
|
81
|
+
}): AtlasClient;
|
|
82
|
+
export interface SyncOptions {
|
|
83
|
+
/** Refresh even if the current token is not yet inside its expiry window. */
|
|
84
|
+
force?: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Refresh and persist the session token — the call a background service worker
|
|
88
|
+
* makes (typically on a `chrome.alarms` tick, or before a token-gated request).
|
|
89
|
+
*
|
|
90
|
+
* A service worker has no long-lived timer of its own — MV3 evicts it — so the
|
|
91
|
+
* web SDK's in-page proactive-refresh loop does not translate. Instead the
|
|
92
|
+
* worker calls this whenever it wakes: it reads the stored token, and if it is
|
|
93
|
+
* within the refresh lead window (or `force`) mints a new one via
|
|
94
|
+
* `POST /v1/client/sessions/:sid/tokens` (bearer-authenticated by the client)
|
|
95
|
+
* and writes it back to `chrome.storage`.
|
|
96
|
+
*
|
|
97
|
+
* Returns the current token (fresh or unchanged), or null when signed out. A
|
|
98
|
+
* failed refresh returns the existing token rather than throwing — the worker
|
|
99
|
+
* can retry on its next tick, and the server rejects a truly-dead token on use.
|
|
100
|
+
*/
|
|
101
|
+
export declare function syncSessionToken(client: AtlasClient, options?: SyncOptions): Promise<string | null>;
|
|
102
|
+
export type { ProviderToken };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decodeTokenClaims = decodeTokenClaims;
|
|
4
|
+
exports.bearerFetch = bearerFetch;
|
|
5
|
+
exports.persistingBearerFetch = persistingBearerFetch;
|
|
6
|
+
exports.createAtlasClient = createAtlasClient;
|
|
7
|
+
exports.syncSessionToken = syncSessionToken;
|
|
8
|
+
const js_1 = require("@atlasauth/js");
|
|
9
|
+
const storage_1 = require("./storage");
|
|
10
|
+
/** Read the unverified claims out of a JWT. Never a security decision — the
|
|
11
|
+
* server validates the signature; this only tells the client which session to
|
|
12
|
+
* refresh and roughly when. Returns null for anything that is not a JWT. */
|
|
13
|
+
function decodeTokenClaims(jwt) {
|
|
14
|
+
const parts = jwt.split('.');
|
|
15
|
+
if (parts.length !== 3)
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Wrap a `fetch` so every request carries the current session token as a bearer.
|
|
26
|
+
* The token is read FRESH from the store on each call — never captured — so a
|
|
27
|
+
* refresh or a sign-out between requests is always reflected. A request made
|
|
28
|
+
* while signed out simply carries no `Authorization` header.
|
|
29
|
+
*/
|
|
30
|
+
function bearerFetch(fetchImpl, store) {
|
|
31
|
+
return async (input, init) => {
|
|
32
|
+
const token = await store.getToken();
|
|
33
|
+
const headers = new Headers(init?.headers);
|
|
34
|
+
if (token)
|
|
35
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
36
|
+
return fetchImpl(input, { ...init, headers });
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Pull a request URL string out of whatever `fetch` was handed. */
|
|
40
|
+
function urlOf(input) {
|
|
41
|
+
if (typeof input === 'string')
|
|
42
|
+
return input;
|
|
43
|
+
if (input instanceof URL)
|
|
44
|
+
return input.toString();
|
|
45
|
+
return input.url;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A {@link bearerFetch} that ALSO captures a freshly-minted session token out of
|
|
49
|
+
* the response and persists it to the store.
|
|
50
|
+
*
|
|
51
|
+
* The web SDK's provider keeps its refreshed JWT in memory, which is exactly
|
|
52
|
+
* what an extension cannot do — the popup that received the token is closed
|
|
53
|
+
* moments later, and the service worker is torn down. So when a token-issuing
|
|
54
|
+
* endpoint (`/v1/client/...`) answers with a `jwt` in its JSON body, we write it
|
|
55
|
+
* back to `chrome.storage`, so the next request from ANY surface — and the next
|
|
56
|
+
* cold start of the service worker — picks up the current token. Scoped to
|
|
57
|
+
* `/v1/client` responses and guarded by JSON parsing, so a non-JSON or unrelated
|
|
58
|
+
* response is left untouched.
|
|
59
|
+
*/
|
|
60
|
+
function persistingBearerFetch(fetchImpl, store) {
|
|
61
|
+
return async (input, init) => {
|
|
62
|
+
const token = await store.getToken();
|
|
63
|
+
const headers = new Headers(init?.headers);
|
|
64
|
+
if (token)
|
|
65
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
66
|
+
const response = await fetchImpl(input, { ...init, headers });
|
|
67
|
+
if (response.ok && /\/v1\/client(\/|\?|$)/.test(urlOf(input))) {
|
|
68
|
+
try {
|
|
69
|
+
const body = (await response.clone().json());
|
|
70
|
+
if (typeof body.jwt === 'string')
|
|
71
|
+
await store.setToken(body.jwt);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Not JSON, or no body (a 204 revoke). Nothing to capture.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return response;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build an {@link AtlasClient}. `background: true` is accepted for parity with
|
|
82
|
+
* the sibling SDKs and to read as intent at the call site; the client behaves
|
|
83
|
+
* the same in a service worker as on a page.
|
|
84
|
+
*/
|
|
85
|
+
function createAtlasClient(options) {
|
|
86
|
+
const store = options.store ?? new storage_1.StorageCache();
|
|
87
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
88
|
+
const fapi = new js_1.FapiClient({
|
|
89
|
+
publishableKey: options.publishableKey,
|
|
90
|
+
baseUrl: options.frontendApi,
|
|
91
|
+
fetchImpl: bearerFetch(fetchImpl, store),
|
|
92
|
+
});
|
|
93
|
+
const client = {
|
|
94
|
+
fapi,
|
|
95
|
+
store,
|
|
96
|
+
getToken: () => store.getToken(),
|
|
97
|
+
setToken: (token) => store.setToken(token),
|
|
98
|
+
getProviderToken: (provider) => getProviderTokenFor(fapi, provider),
|
|
99
|
+
syncSessionToken: (opts) => syncSessionToken(client, opts),
|
|
100
|
+
signOut: async () => {
|
|
101
|
+
const current = await store.getToken();
|
|
102
|
+
const sid = current ? decodeTokenClaims(current)?.sid : undefined;
|
|
103
|
+
if (sid) {
|
|
104
|
+
// Best-effort server revoke; the local token is cleared regardless so
|
|
105
|
+
// the extension never keeps showing a signed-in state after sign-out.
|
|
106
|
+
await fapi.post(`/v1/client/sessions/${sid}/revoke`).catch(() => undefined);
|
|
107
|
+
}
|
|
108
|
+
await store.removeToken();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
return client;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Refresh and persist the session token — the call a background service worker
|
|
115
|
+
* makes (typically on a `chrome.alarms` tick, or before a token-gated request).
|
|
116
|
+
*
|
|
117
|
+
* A service worker has no long-lived timer of its own — MV3 evicts it — so the
|
|
118
|
+
* web SDK's in-page proactive-refresh loop does not translate. Instead the
|
|
119
|
+
* worker calls this whenever it wakes: it reads the stored token, and if it is
|
|
120
|
+
* within the refresh lead window (or `force`) mints a new one via
|
|
121
|
+
* `POST /v1/client/sessions/:sid/tokens` (bearer-authenticated by the client)
|
|
122
|
+
* and writes it back to `chrome.storage`.
|
|
123
|
+
*
|
|
124
|
+
* Returns the current token (fresh or unchanged), or null when signed out. A
|
|
125
|
+
* failed refresh returns the existing token rather than throwing — the worker
|
|
126
|
+
* can retry on its next tick, and the server rejects a truly-dead token on use.
|
|
127
|
+
*/
|
|
128
|
+
async function syncSessionToken(client, options = {}) {
|
|
129
|
+
const current = await client.store.getToken();
|
|
130
|
+
if (!current)
|
|
131
|
+
return null;
|
|
132
|
+
const claims = decodeTokenClaims(current);
|
|
133
|
+
const expiresAt = (0, js_1.readExpiry)(current);
|
|
134
|
+
// Still comfortably fresh and no force → nothing to do, and no needless call.
|
|
135
|
+
if (!options.force && expiresAt !== null) {
|
|
136
|
+
const wait = (0, js_1.msUntilRefresh)({
|
|
137
|
+
token: { jwt: current, expiresAt, sessionId: claims?.sid ?? '' },
|
|
138
|
+
now: Date.now(),
|
|
139
|
+
});
|
|
140
|
+
if (wait > 0)
|
|
141
|
+
return current;
|
|
142
|
+
}
|
|
143
|
+
const sid = claims?.sid;
|
|
144
|
+
if (!sid)
|
|
145
|
+
return current;
|
|
146
|
+
const response = await client.fapi.post(`/v1/client/sessions/${sid}/tokens`);
|
|
147
|
+
if (!response.ok || !response.data?.jwt)
|
|
148
|
+
return current;
|
|
149
|
+
await client.store.setToken(response.data.jwt);
|
|
150
|
+
return response.data.jwt;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* §6.6 token vault. The signed-in user's OWN live access token at a connected
|
|
154
|
+
* provider, so the extension can call e.g. the Google API directly. Returns null
|
|
155
|
+
* when there is no usable token (no linked account / provider granted none).
|
|
156
|
+
*/
|
|
157
|
+
async function getProviderTokenFor(fapi, provider) {
|
|
158
|
+
const response = await fapi.get(`/v1/client/me/external_accounts/${encodeURIComponent(provider)}/token`);
|
|
159
|
+
if (!response.ok || !response.data)
|
|
160
|
+
return null;
|
|
161
|
+
return {
|
|
162
|
+
provider: response.data.provider,
|
|
163
|
+
accessToken: response.data.access_token,
|
|
164
|
+
expiresAt: response.data.expires_at ?? null,
|
|
165
|
+
scopes: response.data.scopes ?? [],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@atlasauth/chrome-extension` — the React-free core.
|
|
3
|
+
*
|
|
4
|
+
* This entry point (the package root) pulls in NO React and NO DOM renderer, so
|
|
5
|
+
* it is safe to import from a background service worker or a content script. It
|
|
6
|
+
* gives you the token store and the FAPI client + session-token lifecycle:
|
|
7
|
+
*
|
|
8
|
+
* import { createAtlasClient, StorageCache } from '@atlasauth/chrome-extension';
|
|
9
|
+
*
|
|
10
|
+
* For the React provider + hooks, import from `@atlasauth/chrome-extension/react`
|
|
11
|
+
* (React is an optional peer). For the background helpers under an explicit,
|
|
12
|
+
* intent-revealing name, `@atlasauth/chrome-extension/background` re-exports them.
|
|
13
|
+
*/
|
|
14
|
+
export * from './storage';
|
|
15
|
+
export * from './client';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* `@atlasauth/chrome-extension` — the React-free core.
|
|
19
|
+
*
|
|
20
|
+
* This entry point (the package root) pulls in NO React and NO DOM renderer, so
|
|
21
|
+
* it is safe to import from a background service worker or a content script. It
|
|
22
|
+
* gives you the token store and the FAPI client + session-token lifecycle:
|
|
23
|
+
*
|
|
24
|
+
* import { createAtlasClient, StorageCache } from '@atlasauth/chrome-extension';
|
|
25
|
+
*
|
|
26
|
+
* For the React provider + hooks, import from `@atlasauth/chrome-extension/react`
|
|
27
|
+
* (React is an optional peer). For the background helpers under an explicit,
|
|
28
|
+
* intent-revealing name, `@atlasauth/chrome-extension/background` re-exports them.
|
|
29
|
+
*/
|
|
30
|
+
__exportStar(require("./storage"), exports);
|
|
31
|
+
__exportStar(require("./client"), exports);
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;;GAYG;AACH,4CAA0B;AAC1B,2CAAyB"}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type AtlasProviderProps } from '@atlasauth/react';
|
|
2
|
+
import { type TokenStore } from './storage';
|
|
3
|
+
/**
|
|
4
|
+
* `<AtlasProvider>` for a Chrome/MV3 extension.
|
|
5
|
+
*
|
|
6
|
+
* A thin wrapper over `@atlasauth/react`'s provider. It reuses the entire React
|
|
7
|
+
* SDK — the same hooks (`useAuth`, `useUser`, …), the same components — and
|
|
8
|
+
* changes exactly ONE thing: how the session travels. The web provider assumes
|
|
9
|
+
* cookies (`credentials: 'include'`); an extension has none, so this wrapper
|
|
10
|
+
* injects a `fetchImpl` that instead
|
|
11
|
+
* 1. attaches the token from the extension's {@link TokenStore} as a bearer, and
|
|
12
|
+
* 2. captures any freshly-minted token out of a `/v1/client` response back
|
|
13
|
+
* into `chrome.storage`, so it survives the popup closing and the service
|
|
14
|
+
* worker being torn down.
|
|
15
|
+
*
|
|
16
|
+
* React and `@atlasauth/react` are OPTIONAL peer dependencies — this module is
|
|
17
|
+
* only reached through the `@atlasauth/chrome-extension/react` entry point. A
|
|
18
|
+
* background service worker imports the React-free core (`createAtlasClient`,
|
|
19
|
+
* `syncSessionToken`, `StorageCache`) from the package root instead and never
|
|
20
|
+
* pulls React in.
|
|
21
|
+
*
|
|
22
|
+
* The token must already be in the store (seed it with `client.setToken(jwt)`
|
|
23
|
+
* after a hosted sign-in, or let a prior session persist it). This provider does
|
|
24
|
+
* not itself drive the sign-in UI flow.
|
|
25
|
+
*/
|
|
26
|
+
export interface ChromeAtlasProviderProps extends Omit<AtlasProviderProps, 'fetchImpl'> {
|
|
27
|
+
/** Where the token lives. Defaults to `chrome.storage.local` via {@link StorageCache}. */
|
|
28
|
+
store?: TokenStore;
|
|
29
|
+
/** Base `fetch` to wrap (before bearer/persist). Defaults to the platform `fetch`. */
|
|
30
|
+
fetchImpl?: typeof fetch;
|
|
31
|
+
}
|
|
32
|
+
export declare function AtlasProvider(props: ChromeAtlasProviderProps): import("react").FunctionComponentElement<AtlasProviderProps>;
|
|
33
|
+
export * from '@atlasauth/react';
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.AtlasProvider = AtlasProvider;
|
|
18
|
+
const react_1 = require("react");
|
|
19
|
+
const react_2 = require("@atlasauth/react");
|
|
20
|
+
const client_1 = require("./client");
|
|
21
|
+
const storage_1 = require("./storage");
|
|
22
|
+
function AtlasProvider(props) {
|
|
23
|
+
const { store, fetchImpl, ...rest } = props;
|
|
24
|
+
const wrappedFetch = (0, react_1.useMemo)(() => (0, client_1.persistingBearerFetch)(fetchImpl ?? fetch, store ?? new storage_1.StorageCache()), [store, fetchImpl]);
|
|
25
|
+
return (0, react_1.createElement)(react_2.AtlasProvider, { ...rest, fetchImpl: wrappedFetch });
|
|
26
|
+
}
|
|
27
|
+
// Re-export the React SDK surface so extension code has one import site for the
|
|
28
|
+
// provider AND the hooks/components it renders inside it.
|
|
29
|
+
__exportStar(require("@atlasauth/react"), exports);
|
|
30
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAmCA,sCASC;AA5CD,iCAA+C;AAC/C,4CAAgG;AAChG,qCAAiD;AACjD,uCAA0D;AAgC1D,SAAgB,aAAa,CAAC,KAA+B;IAC3D,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IAE5C,MAAM,YAAY,GAAG,IAAA,eAAO,EAC1B,GAAG,EAAE,CAAC,IAAA,8BAAqB,EAAC,SAAS,IAAI,KAAK,EAAE,KAAK,IAAI,IAAI,sBAAY,EAAE,CAAC,EAC5E,CAAC,KAAK,EAAE,SAAS,CAAC,CACnB,CAAC;IAEF,OAAO,IAAA,qBAAa,EAAC,qBAAkB,EAAE,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC;AACjF,CAAC;AAED,gFAAgF;AAChF,0DAA0D;AAC1D,mDAAiC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The extension token store.
|
|
3
|
+
*
|
|
4
|
+
* A browser extension surface — popup, options page, side panel, a content
|
|
5
|
+
* script, and the background service worker — has NO cookie jar to the Atlas
|
|
6
|
+
* FAPI origin. A cross-origin `fetch` from an extension cannot lean on the
|
|
7
|
+
* browser attaching a session cookie the way a first-party web app does, so the
|
|
8
|
+
* whole cookie-based session the web SDK relies on is simply unavailable. The
|
|
9
|
+
* session token therefore has to live somewhere the extension itself controls,
|
|
10
|
+
* and be attached to every request by hand as a bearer.
|
|
11
|
+
*
|
|
12
|
+
* `chrome.storage.local` is that place:
|
|
13
|
+
* - it SURVIVES the service worker being torn down between events (a plain
|
|
14
|
+
* module-level variable in a service worker does not — MV3 kills the worker
|
|
15
|
+
* when idle and re-imports it on the next event, losing memory state);
|
|
16
|
+
* - it is readable from EVERY extension surface, so the popup that signs the
|
|
17
|
+
* user in and the background worker that refreshes the token share one copy.
|
|
18
|
+
*
|
|
19
|
+
* This module is the single owner of "where the token lives". Everything else
|
|
20
|
+
* (the FAPI client, the background sync, the React provider) reads and writes it
|
|
21
|
+
* only through the {@link TokenStore} interface below.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The minimal, structural shape of the two `chrome.storage.local` methods this
|
|
25
|
+
* package uses. Declared here — rather than pulled from `@types/chrome` — so the
|
|
26
|
+
* package type-checks with NO extra type dependency; at runtime the extension
|
|
27
|
+
* platform supplies the real object. MV3's `chrome.storage` methods return a
|
|
28
|
+
* Promise when called without a callback, which is the form used throughout.
|
|
29
|
+
*/
|
|
30
|
+
export interface ChromeStorageArea {
|
|
31
|
+
get(keys: string | string[] | null): Promise<Record<string, unknown>>;
|
|
32
|
+
set(items: Record<string, unknown>): Promise<void>;
|
|
33
|
+
remove(keys: string | string[]): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The three-method contract the rest of the SDK depends on. `StorageCache` is
|
|
37
|
+
* the shipped `chrome.storage.local` implementation; tests and non-extension
|
|
38
|
+
* prototypes can supply any object with this shape.
|
|
39
|
+
*/
|
|
40
|
+
export interface TokenStore {
|
|
41
|
+
/** The stored session token, or null when signed out. */
|
|
42
|
+
getToken(): Promise<string | null>;
|
|
43
|
+
/** Persist the session token. Called after sign-in and on every refresh. */
|
|
44
|
+
setToken(token: string): Promise<void>;
|
|
45
|
+
/** Drop the token. Called on sign-out and when a session is rejected. */
|
|
46
|
+
removeToken(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** The storage key the token is kept under. Namespaced to avoid collisions. */
|
|
49
|
+
export declare const DEFAULT_TOKEN_KEY = "__atlas_session_token";
|
|
50
|
+
export interface StorageCacheOptions {
|
|
51
|
+
/** Override the storage key (e.g. to isolate multiple instances). */
|
|
52
|
+
key?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The backing storage area. Defaults to `chrome.storage.local`, resolved
|
|
55
|
+
* lazily on first use. Inject an in-memory area ({@link createMemoryStorageArea})
|
|
56
|
+
* in tests, or `chrome.storage.session` / `.sync` to change persistence.
|
|
57
|
+
*/
|
|
58
|
+
area?: ChromeStorageArea;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A token cache over `chrome.storage.local`. The session token is a single
|
|
62
|
+
* string value under one key — the extension analogue of the web SDK's cookie.
|
|
63
|
+
*/
|
|
64
|
+
export declare class StorageCache implements TokenStore {
|
|
65
|
+
private readonly key;
|
|
66
|
+
private area;
|
|
67
|
+
constructor(options?: StorageCacheOptions);
|
|
68
|
+
private storage;
|
|
69
|
+
getToken(): Promise<string | null>;
|
|
70
|
+
setToken(token: string): Promise<void>;
|
|
71
|
+
removeToken(): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* An in-memory `ChromeStorageArea`, for tests and for prototyping outside an
|
|
75
|
+
* extension. It implements the same async contract as the real thing but keeps
|
|
76
|
+
* everything in a `Map`, so it does NOT survive a process (or service-worker)
|
|
77
|
+
* restart — never ship it as the store in production.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createMemoryStorageArea(): ChromeStorageArea;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The extension token store.
|
|
4
|
+
*
|
|
5
|
+
* A browser extension surface — popup, options page, side panel, a content
|
|
6
|
+
* script, and the background service worker — has NO cookie jar to the Atlas
|
|
7
|
+
* FAPI origin. A cross-origin `fetch` from an extension cannot lean on the
|
|
8
|
+
* browser attaching a session cookie the way a first-party web app does, so the
|
|
9
|
+
* whole cookie-based session the web SDK relies on is simply unavailable. The
|
|
10
|
+
* session token therefore has to live somewhere the extension itself controls,
|
|
11
|
+
* and be attached to every request by hand as a bearer.
|
|
12
|
+
*
|
|
13
|
+
* `chrome.storage.local` is that place:
|
|
14
|
+
* - it SURVIVES the service worker being torn down between events (a plain
|
|
15
|
+
* module-level variable in a service worker does not — MV3 kills the worker
|
|
16
|
+
* when idle and re-imports it on the next event, losing memory state);
|
|
17
|
+
* - it is readable from EVERY extension surface, so the popup that signs the
|
|
18
|
+
* user in and the background worker that refreshes the token share one copy.
|
|
19
|
+
*
|
|
20
|
+
* This module is the single owner of "where the token lives". Everything else
|
|
21
|
+
* (the FAPI client, the background sync, the React provider) reads and writes it
|
|
22
|
+
* only through the {@link TokenStore} interface below.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.StorageCache = exports.DEFAULT_TOKEN_KEY = void 0;
|
|
26
|
+
exports.createMemoryStorageArea = createMemoryStorageArea;
|
|
27
|
+
/** The storage key the token is kept under. Namespaced to avoid collisions. */
|
|
28
|
+
exports.DEFAULT_TOKEN_KEY = '__atlas_session_token';
|
|
29
|
+
/**
|
|
30
|
+
* Resolve `chrome.storage.local` off the global. Done lazily (on first access,
|
|
31
|
+
* not at construction) so importing or wiring the SDK never throws in a context
|
|
32
|
+
* where `chrome` is absent — only an actual read/write does.
|
|
33
|
+
*/
|
|
34
|
+
function resolveChromeLocal() {
|
|
35
|
+
const runtime = globalThis.chrome;
|
|
36
|
+
const local = runtime?.storage?.local;
|
|
37
|
+
if (!local) {
|
|
38
|
+
throw new Error('@atlasauth/chrome-extension: chrome.storage.local is unavailable. Run this ' +
|
|
39
|
+
'inside an extension with the "storage" permission in its manifest, or pass ' +
|
|
40
|
+
'a custom `area` to StorageCache (e.g. createMemoryStorageArea()).');
|
|
41
|
+
}
|
|
42
|
+
return local;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A token cache over `chrome.storage.local`. The session token is a single
|
|
46
|
+
* string value under one key — the extension analogue of the web SDK's cookie.
|
|
47
|
+
*/
|
|
48
|
+
class StorageCache {
|
|
49
|
+
key;
|
|
50
|
+
area;
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this.key = options.key ?? exports.DEFAULT_TOKEN_KEY;
|
|
53
|
+
// Held null until first use so construction never touches `chrome`.
|
|
54
|
+
this.area = options.area ?? null;
|
|
55
|
+
}
|
|
56
|
+
storage() {
|
|
57
|
+
if (!this.area)
|
|
58
|
+
this.area = resolveChromeLocal();
|
|
59
|
+
return this.area;
|
|
60
|
+
}
|
|
61
|
+
async getToken() {
|
|
62
|
+
const result = await this.storage().get(this.key);
|
|
63
|
+
const value = result[this.key];
|
|
64
|
+
return typeof value === 'string' ? value : null;
|
|
65
|
+
}
|
|
66
|
+
async setToken(token) {
|
|
67
|
+
await this.storage().set({ [this.key]: token });
|
|
68
|
+
}
|
|
69
|
+
async removeToken() {
|
|
70
|
+
await this.storage().remove(this.key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.StorageCache = StorageCache;
|
|
74
|
+
/**
|
|
75
|
+
* An in-memory `ChromeStorageArea`, for tests and for prototyping outside an
|
|
76
|
+
* extension. It implements the same async contract as the real thing but keeps
|
|
77
|
+
* everything in a `Map`, so it does NOT survive a process (or service-worker)
|
|
78
|
+
* restart — never ship it as the store in production.
|
|
79
|
+
*/
|
|
80
|
+
function createMemoryStorageArea() {
|
|
81
|
+
const map = new Map();
|
|
82
|
+
const pick = (keys) => {
|
|
83
|
+
if (keys === null)
|
|
84
|
+
return Object.fromEntries(map);
|
|
85
|
+
const list = Array.isArray(keys) ? keys : [keys];
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const k of list) {
|
|
88
|
+
if (map.has(k))
|
|
89
|
+
out[k] = map.get(k);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
get: (keys) => Promise.resolve(pick(keys)),
|
|
95
|
+
set: (items) => {
|
|
96
|
+
for (const [k, v] of Object.entries(items))
|
|
97
|
+
map.set(k, v);
|
|
98
|
+
return Promise.resolve();
|
|
99
|
+
},
|
|
100
|
+
remove: (keys) => {
|
|
101
|
+
for (const k of Array.isArray(keys) ? keys : [keys])
|
|
102
|
+
map.delete(k);
|
|
103
|
+
return Promise.resolve();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atlasauth/chrome-extension",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./background": {
|
|
12
|
+
"types": "./dist/background.d.ts",
|
|
13
|
+
"default": "./dist/background.js"
|
|
14
|
+
},
|
|
15
|
+
"./react": {
|
|
16
|
+
"types": "./dist/react.d.ts",
|
|
17
|
+
"default": "./dist/react.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@atlasauth/js": "0.1.4",
|
|
22
|
+
"@atlasauth/react": "0.1.13"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"peerDependenciesMeta": {
|
|
28
|
+
"react": {
|
|
29
|
+
"optional": true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^18.3.12",
|
|
34
|
+
"react": "^18.3.1",
|
|
35
|
+
"typescript": "^5.6.3",
|
|
36
|
+
"vitest": "^2.1.4"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
}
|
|
50
|
+
}
|