@andco/sdk 0.0.1

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.
Files changed (63) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +176 -0
  3. package/dist/auth.d.ts +71 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +98 -0
  6. package/dist/browser/controller.d.ts +76 -0
  7. package/dist/browser/controller.d.ts.map +1 -0
  8. package/dist/browser/controller.js +215 -0
  9. package/dist/browser/frame.d.ts +65 -0
  10. package/dist/browser/frame.d.ts.map +1 -0
  11. package/dist/browser/frame.js +237 -0
  12. package/dist/browser/index.d.ts +52 -0
  13. package/dist/browser/index.d.ts.map +1 -0
  14. package/dist/browser/index.js +117 -0
  15. package/dist/browser/popup.d.ts +105 -0
  16. package/dist/browser/popup.d.ts.map +1 -0
  17. package/dist/browser/popup.js +177 -0
  18. package/dist/cli/index.d.ts +47 -0
  19. package/dist/cli/index.d.ts.map +1 -0
  20. package/dist/cli/index.js +72 -0
  21. package/dist/cli/server.d.ts +11 -0
  22. package/dist/cli/server.d.ts.map +1 -0
  23. package/dist/cli/server.js +91 -0
  24. package/dist/client.d.ts +135 -0
  25. package/dist/client.d.ts.map +1 -0
  26. package/dist/client.js +210 -0
  27. package/dist/config.d.ts +82 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +109 -0
  30. package/dist/credentials.d.ts +76 -0
  31. package/dist/credentials.d.ts.map +1 -0
  32. package/dist/credentials.js +0 -0
  33. package/dist/errors.d.ts +165 -0
  34. package/dist/errors.d.ts.map +1 -0
  35. package/dist/errors.js +197 -0
  36. package/dist/index.d.ts +13 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +11 -0
  39. package/dist/intents.d.ts +89 -0
  40. package/dist/intents.d.ts.map +1 -0
  41. package/dist/intents.js +157 -0
  42. package/dist/oauth.d.ts +147 -0
  43. package/dist/oauth.d.ts.map +1 -0
  44. package/dist/oauth.js +348 -0
  45. package/dist/presenter.d.ts +36 -0
  46. package/dist/presenter.d.ts.map +1 -0
  47. package/dist/presenter.js +1 -0
  48. package/dist/rest.d.ts +58 -0
  49. package/dist/rest.d.ts.map +1 -0
  50. package/dist/rest.js +48 -0
  51. package/dist/server/index.d.ts +21 -0
  52. package/dist/server/index.d.ts.map +1 -0
  53. package/dist/server/index.js +27 -0
  54. package/dist/server-metadata.generated.d.ts +4 -0
  55. package/dist/server-metadata.generated.d.ts.map +1 -0
  56. package/dist/server-metadata.generated.js +49 -0
  57. package/dist/session-store.d.ts +83 -0
  58. package/dist/session-store.d.ts.map +1 -0
  59. package/dist/session-store.js +186 -0
  60. package/dist/storage.d.ts +61 -0
  61. package/dist/storage.d.ts.map +1 -0
  62. package/dist/storage.js +47 -0
  63. package/package.json +56 -0
@@ -0,0 +1,21 @@
1
+ import { AndcoClient, type AndcoClientOptions } from "../client.js";
2
+ export type AndcoServerOptions = AndcoClientOptions;
3
+ /**
4
+ * Creates an Andco Instance for a server.
5
+ *
6
+ * It holds no session. Credentials arrive per request through `with(...)`, which is what makes one
7
+ * module-scoped instance safe to share across concurrent requests: there is no field for one user's
8
+ * credentials to leak into another's response.
9
+ *
10
+ * It refuses to run in a browser, because a confidential client secret must never reach one.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * export const andco = createAndcoInstanceForServer({ clientId, clientSecret });
15
+ *
16
+ * // in a request handler
17
+ * const { data } = await bank.use(andco.with(session)).client.http.GET("/accounts").throwOnError();
18
+ * ```
19
+ */
20
+ export declare function createAndcoInstanceForServer(options: AndcoServerOptions): AndcoClient;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGpE,MAAM,MAAM,kBAAkB,GAAG,kBAAkB,CAAC;AAEpD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAOrF"}
@@ -0,0 +1,27 @@
1
+ import { AndcoClient } from "../client.js";
2
+ import { ANDCO_ERROR_CODES, AndcoError } from "../errors.js";
3
+ /**
4
+ * Creates an Andco Instance for a server.
5
+ *
6
+ * It holds no session. Credentials arrive per request through `with(...)`, which is what makes one
7
+ * module-scoped instance safe to share across concurrent requests: there is no field for one user's
8
+ * credentials to leak into another's response.
9
+ *
10
+ * It refuses to run in a browser, because a confidential client secret must never reach one.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * export const andco = createAndcoInstanceForServer({ clientId, clientSecret });
15
+ *
16
+ * // in a request handler
17
+ * const { data } = await bank.use(andco.with(session)).client.http.GET("/accounts").throwOnError();
18
+ * ```
19
+ */
20
+ export function createAndcoInstanceForServer(options) {
21
+ if (typeof window !== "undefined") {
22
+ throw new AndcoError(ANDCO_ERROR_CODES.BROWSER_FORBIDDEN, {
23
+ message: "createAndcoInstanceForServer cannot run in a browser",
24
+ });
25
+ }
26
+ return new AndcoClient(options);
27
+ }
@@ -0,0 +1,4 @@
1
+ import type * as oidc from "openid-client";
2
+ /** The baked metadata, re-rooted on one deployment's issuer. */
3
+ export declare function andcoServerMetadata(issuer: string): oidc.ServerMetadata;
4
+ //# sourceMappingURL=server-metadata.generated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-metadata.generated.d.ts","sourceRoot":"","sources":["../src/server-metadata.generated.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,KAAK,IAAI,MAAM,eAAe,CAAC;AAsC3C,gEAAgE;AAChE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,cAAc,CAEvE"}
@@ -0,0 +1,49 @@
1
+ // WARNING: GENERATED FILE. DO NOT EDIT BY HAND.
2
+ //
3
+ // The Authorization Server's own description of itself, read once at build time instead of on every
4
+ // application's first sign-in. ADR-0021 explains why it is baked and what breaks when it is stale:
5
+ // every value here is one the server owns, and a wrong one fails silently at the last step of a
6
+ // sign-in. `{issuer}` is substituted with the configured `endpointAuth`.
7
+ //
8
+ // Regenerate: pnpm --filter @andco/sdk run refresh:server-metadata
9
+ // Source: https://auth.localhost/auth/v1/.well-known/openid-configuration
10
+ // Generated: 2026-09-19T01:19:04.372Z
11
+ const TEMPLATE = {
12
+ issuer: "{issuer}",
13
+ authorization_endpoint: "{issuer}/oauth/authorize",
14
+ token_endpoint: "{issuer}/oauth/token",
15
+ device_authorization_endpoint: "{issuer}/oauth/device/authorize",
16
+ jwks_uri: "{issuer}/.well-known/jwks.json",
17
+ userinfo_endpoint: "{issuer}/oauth/userinfo",
18
+ registration_endpoint: "{issuer}/oauth/clients/register",
19
+ scopes_supported: ["openid", "profile", "email", "phone", "offline_access"],
20
+ response_types_supported: ["code"],
21
+ response_modes_supported: ["query"],
22
+ grant_types_supported: ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
23
+ subject_types_supported: ["public"],
24
+ id_token_signing_alg_values_supported: ["RS256", "HS256", "ES256"],
25
+ token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post", "none"],
26
+ claims_supported: [
27
+ "sub",
28
+ "aud",
29
+ "iss",
30
+ "exp",
31
+ "iat",
32
+ "auth_time",
33
+ "nonce",
34
+ "email",
35
+ "email_verified",
36
+ "phone_number",
37
+ "phone_number_verified",
38
+ "name",
39
+ "picture",
40
+ "preferred_username",
41
+ "updated_at",
42
+ ],
43
+ code_challenge_methods_supported: ["S256", "plain"],
44
+ pushed_authorization_request_endpoint: "{issuer}/oauth/par",
45
+ };
46
+ /** The baked metadata, re-rooted on one deployment's issuer. */
47
+ export function andcoServerMetadata(issuer) {
48
+ return JSON.parse(JSON.stringify(TEMPLATE).replaceAll("{issuer}", issuer));
49
+ }
@@ -0,0 +1,83 @@
1
+ import type { AndcoConfig } from "./config.js";
2
+ import { type AndcoCredentials, type AndcoSession } from "./credentials.js";
3
+ import { AndcoError, Result } from "./errors.js";
4
+ import type { AndcoOAuth } from "./oauth.js";
5
+ import { type AndcoLock, type AndcoStorage } from "./storage.js";
6
+ /** Reads a session that arrived from somewhere other than storage, such as a URL callback. */
7
+ export type AndcoSessionSource = () => Promise<Result<AndcoSession | null>>;
8
+ export type AndcoSessionStoreOptions = {
9
+ config: AndcoConfig;
10
+ oauth: AndcoOAuth;
11
+ storage?: AndcoStorage;
12
+ /** A session the caller already resolved. Makes the first snapshot real instead of unresolved. */
13
+ initialSession?: AndcoSession | null;
14
+ /** Serializes refresh beyond this runtime. Defaults to in-process only. */
15
+ lock?: AndcoLock;
16
+ /** Consulted once during the load, before storage. Used for URL callbacks. */
17
+ source?: AndcoSessionSource;
18
+ /** Whether a loaded session is written back to storage. */
19
+ persist?: boolean;
20
+ };
21
+ /**
22
+ * The credential as a resource, and the only stateful object in the SDK.
23
+ *
24
+ * Two rules make it safe to construct anywhere, including inside a render that a framework may
25
+ * throw away:
26
+ *
27
+ * Construction is pure. Nothing is read, fetched, or written when the store is built.
28
+ *
29
+ * The load is lazy and starts on the first *read* — a subscription, an await on `ready`, or a token
30
+ * resolution. It never starts from `getSnapshot`. React calls the snapshot getter during render and
31
+ * the subscriber in commit, so a render that is never committed never triggers a load. That is what
32
+ * makes a discarded construction inert, and it matters most for an authorization code, which is
33
+ * single-use: a discarded store that exchanged one would leave the retained store with nothing.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const session = useSyncExternalStore(
38
+ * store.subscribe.bind(store),
39
+ * store.getSnapshot.bind(store),
40
+ * store.getServerSnapshot.bind(store),
41
+ * );
42
+ * ```
43
+ */
44
+ export declare class AndcoSessionStore implements AndcoCredentials {
45
+ #private;
46
+ constructor(options: AndcoSessionStoreOptions);
47
+ /**
48
+ * The current value. `undefined` is Instance Readiness: the load has not finished. It is not the
49
+ * same as `null`, which means there is no session, and an application that renders them the same
50
+ * way flashes a signed-out state on every load.
51
+ *
52
+ * Pure by contract. Reading this never starts the load.
53
+ */
54
+ getSnapshot(): AndcoSession | null | undefined;
55
+ /** What a server render sees: the hydration value, or unresolved. Never touches storage. */
56
+ getServerSnapshot(): AndcoSession | null | undefined;
57
+ /** Observes changes and starts the load. The returned function is the unsubscribe. */
58
+ subscribe(listener: () => void): () => void;
59
+ /**
60
+ * Settles when the initial load finishes.
61
+ *
62
+ * Only code that reads the synchronous snapshot needs this — a test asserting there is no
63
+ * session, or a script branching before it does work. The ordinary path needs nothing, because
64
+ * `accessTokenFor` awaits the load itself.
65
+ */
66
+ get ready(): Promise<Result<void>>;
67
+ /**
68
+ * Resolves a token for one Resource Indicator, refreshing when the current one is stale.
69
+ *
70
+ * Refresh is lazy and never scheduled. No timer exists anywhere in the SDK, which is what lets a
71
+ * discarded instance be inert and removes any need for instance identity.
72
+ */
73
+ accessTokenFor(resource: string): Promise<string | null>;
74
+ /**
75
+ * Replaces the stored session. This is the supported way to restore a saved credential, which
76
+ * previously required writing a JSON blob into the SDK's private storage key because no such
77
+ * method existed.
78
+ */
79
+ set(session: AndcoSession | null): Promise<Result<void>>;
80
+ }
81
+ /** Raised when a store is asked for something only a browser can provide. */
82
+ export declare function browserRequired(): AndcoError;
83
+ //# sourceMappingURL=session-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,YAAY,EAAuB,MAAM,kBAAkB,CAAC;AACjG,OAAO,EAAqB,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,YAAY,EAAgC,MAAM,cAAc,CAAC;AAE/F,8FAA8F;AAC9F,MAAM,MAAM,kBAAkB,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC;AAE5E,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,kGAAkG;IAClG,cAAc,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACrC,2EAA2E;IAC3E,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,2DAA2D;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,iBAAkB,YAAW,gBAAgB;;gBAWrC,OAAO,EAAE,wBAAwB;IASpD;;;;;;OAMG;IACI,WAAW,IAAI,YAAY,GAAG,IAAI,GAAG,SAAS;IAIrD,4FAA4F;IACrF,iBAAiB,IAAI,YAAY,GAAG,IAAI,GAAG,SAAS;IAI3D,sFAAsF;IAC/E,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAQlD;;;;;;OAMG;IACH,IAAW,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAExC;IAED;;;;;OAKG;IACU,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAsBrE;;;;OAIG;IACU,GAAG,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CAsDtE;AAgBD,6EAA6E;AAC7E,wBAAgB,eAAe,IAAI,UAAU,CAE5C"}
@@ -0,0 +1,186 @@
1
+ import { isExpired, tokenFor } from "./credentials.js";
2
+ import { ANDCO_ERROR_CODES, AndcoError, Result } from "./errors.js";
3
+ import { InProcessLock, MemoryStorage } from "./storage.js";
4
+ /**
5
+ * The credential as a resource, and the only stateful object in the SDK.
6
+ *
7
+ * Two rules make it safe to construct anywhere, including inside a render that a framework may
8
+ * throw away:
9
+ *
10
+ * Construction is pure. Nothing is read, fetched, or written when the store is built.
11
+ *
12
+ * The load is lazy and starts on the first *read* — a subscription, an await on `ready`, or a token
13
+ * resolution. It never starts from `getSnapshot`. React calls the snapshot getter during render and
14
+ * the subscriber in commit, so a render that is never committed never triggers a load. That is what
15
+ * makes a discarded construction inert, and it matters most for an authorization code, which is
16
+ * single-use: a discarded store that exchanged one would leave the retained store with nothing.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const session = useSyncExternalStore(
21
+ * store.subscribe.bind(store),
22
+ * store.getSnapshot.bind(store),
23
+ * store.getServerSnapshot.bind(store),
24
+ * );
25
+ * ```
26
+ */
27
+ export class AndcoSessionStore {
28
+ #options;
29
+ #storage;
30
+ #lock;
31
+ #listeners = new Set();
32
+ #key;
33
+ #hydrated;
34
+ #snapshot;
35
+ #load;
36
+ constructor(options) {
37
+ this.#options = options;
38
+ this.#storage = options.storage ?? new MemoryStorage();
39
+ this.#lock = options.lock ?? new InProcessLock();
40
+ this.#key = `andco.session.${options.config.clientId}`;
41
+ this.#hydrated = options.initialSession;
42
+ this.#snapshot = options.initialSession;
43
+ }
44
+ /**
45
+ * The current value. `undefined` is Instance Readiness: the load has not finished. It is not the
46
+ * same as `null`, which means there is no session, and an application that renders them the same
47
+ * way flashes a signed-out state on every load.
48
+ *
49
+ * Pure by contract. Reading this never starts the load.
50
+ */
51
+ getSnapshot() {
52
+ return this.#snapshot;
53
+ }
54
+ /** What a server render sees: the hydration value, or unresolved. Never touches storage. */
55
+ getServerSnapshot() {
56
+ return this.#hydrated;
57
+ }
58
+ /** Observes changes and starts the load. The returned function is the unsubscribe. */
59
+ subscribe(listener) {
60
+ this.#listeners.add(listener);
61
+ void this.#ensureLoaded();
62
+ return () => {
63
+ this.#listeners.delete(listener);
64
+ };
65
+ }
66
+ /**
67
+ * Settles when the initial load finishes.
68
+ *
69
+ * Only code that reads the synchronous snapshot needs this — a test asserting there is no
70
+ * session, or a script branching before it does work. The ordinary path needs nothing, because
71
+ * `accessTokenFor` awaits the load itself.
72
+ */
73
+ get ready() {
74
+ return this.#ensureLoaded();
75
+ }
76
+ /**
77
+ * Resolves a token for one Resource Indicator, refreshing when the current one is stale.
78
+ *
79
+ * Refresh is lazy and never scheduled. No timer exists anywhere in the SDK, which is what lets a
80
+ * discarded instance be inert and removes any need for instance identity.
81
+ */
82
+ async accessTokenFor(resource) {
83
+ await this.#ensureLoaded();
84
+ const current = this.#snapshot;
85
+ if (!current)
86
+ return null;
87
+ if (!isExpired(current))
88
+ return tokenFor(current, resource);
89
+ // Serialized per resource: refreshing one Resource Server's token must not block another's.
90
+ const refreshed = await this.#lock.acquire(`${this.#key}:${resource}`, async () => {
91
+ const latest = this.#snapshot;
92
+ if (!latest)
93
+ return null;
94
+ // Another holder of the lock may have refreshed while this one waited.
95
+ if (!isExpired(latest))
96
+ return latest;
97
+ if (!latest.refreshToken)
98
+ return null;
99
+ const result = await this.#options.oauth.refresh(latest.refreshToken);
100
+ if (result.error)
101
+ return null;
102
+ await this.#write(result.data);
103
+ return result.data;
104
+ });
105
+ return refreshed ? tokenFor(refreshed, resource) : null;
106
+ }
107
+ /**
108
+ * Replaces the stored session. This is the supported way to restore a saved credential, which
109
+ * previously required writing a JSON blob into the SDK's private storage key because no such
110
+ * method existed.
111
+ */
112
+ async set(session) {
113
+ // A caller setting a session has answered the load, so mark it settled rather than racing it.
114
+ this.#load ??= Promise.resolve(Result.ok(undefined));
115
+ try {
116
+ await this.#write(session);
117
+ return Result.ok(undefined);
118
+ }
119
+ catch (cause) {
120
+ return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.STORAGE_FAILED));
121
+ }
122
+ }
123
+ #ensureLoaded() {
124
+ this.#load ??= this.#loadOnce();
125
+ return this.#load;
126
+ }
127
+ async #loadOnce() {
128
+ try {
129
+ const fromSource = this.#options.source ? await this.#options.source() : null;
130
+ if (fromSource?.error) {
131
+ this.#publish(this.#snapshot ?? null);
132
+ return Result.fail(fromSource.error);
133
+ }
134
+ if (fromSource?.data) {
135
+ await this.#write(fromSource.data);
136
+ return Result.ok(undefined);
137
+ }
138
+ if (this.#hydrated !== undefined) {
139
+ this.#publish(this.#hydrated);
140
+ return Result.ok(undefined);
141
+ }
142
+ const stored = await this.#storage.getItem(this.#key);
143
+ this.#publish(stored ? parseSession(stored) : null);
144
+ return Result.ok(undefined);
145
+ }
146
+ catch (cause) {
147
+ this.#publish(this.#snapshot ?? null);
148
+ return Result.fail(AndcoError.from(cause, ANDCO_ERROR_CODES.STORAGE_FAILED));
149
+ }
150
+ }
151
+ async #write(session) {
152
+ if (this.#options.persist !== false) {
153
+ if (session)
154
+ await this.#storage.setItem(this.#key, JSON.stringify(session));
155
+ else
156
+ await this.#storage.removeItem(this.#key);
157
+ }
158
+ this.#publish(session);
159
+ }
160
+ #publish(session) {
161
+ this.#snapshot = session;
162
+ for (const listener of [...this.#listeners])
163
+ listener();
164
+ }
165
+ }
166
+ /** Reads a persisted session, treating anything unrecognisable as absent rather than throwing. */
167
+ function parseSession(value) {
168
+ try {
169
+ const parsed = JSON.parse(value);
170
+ if (!parsed || typeof parsed !== "object")
171
+ return null;
172
+ const candidate = parsed;
173
+ if (typeof candidate.accessToken !== "string" || typeof candidate.expiresAt !== "number")
174
+ return null;
175
+ if (!candidate.user || typeof candidate.user.id !== "string")
176
+ return null;
177
+ return candidate;
178
+ }
179
+ catch {
180
+ return null;
181
+ }
182
+ }
183
+ /** Raised when a store is asked for something only a browser can provide. */
184
+ export function browserRequired() {
185
+ return new AndcoError(ANDCO_ERROR_CODES.BROWSER_REQUIRED);
186
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Where an Andco session is persisted.
3
+ *
4
+ * Every operation may answer synchronously or with a promise. The synchronous-only contract this
5
+ * replaces is the reason the Andco CLI had to build its own asynchronous credential store and then
6
+ * bridge it by writing a JSON blob into the SDK's private storage key: a keychain cannot answer
7
+ * synchronously, so there was no legal way to supply one.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const secureStore: AndcoStorage = {
12
+ * getItem: (key) => SecureStore.getItemAsync(key),
13
+ * setItem: (key, value) => SecureStore.setItemAsync(key, value),
14
+ * removeItem: (key) => SecureStore.deleteItemAsync(key),
15
+ * };
16
+ * ```
17
+ */
18
+ export interface AndcoStorage {
19
+ getItem(key: string): string | null | Promise<string | null>;
20
+ setItem(key: string, value: string): void | Promise<void>;
21
+ removeItem(key: string): void | Promise<void>;
22
+ }
23
+ /** Keeps a session only for the lifetime of the process. The default outside a browser. */
24
+ export declare class MemoryStorage implements AndcoStorage {
25
+ #private;
26
+ getItem(key: string): string | null;
27
+ setItem(key: string, value: string): void;
28
+ removeItem(key: string): void;
29
+ }
30
+ /** Adapts a Web Storage object. Its synchronous methods satisfy the asynchronous-capable contract. */
31
+ export declare class WebStorage implements AndcoStorage {
32
+ private readonly storage;
33
+ constructor(storage: Storage);
34
+ getItem(key: string): string | null;
35
+ setItem(key: string, value: string): void;
36
+ removeItem(key: string): void;
37
+ }
38
+ /**
39
+ * Serializes an operation per key.
40
+ *
41
+ * Refresh tokens rotate, so two concurrent refreshes destroy a session: the first rotates the token
42
+ * and every other one fails against a token that no longer exists. In-process single-flight covers
43
+ * one runtime; this covers several, which is what a serverless deployment and multiple browser tabs
44
+ * need. Not offering it is why both example backends invented their own locking, differently.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const lock: AndcoLock = {
49
+ * acquire: (key, operation) => redlock.using([`andco:${key}`], 5000, operation),
50
+ * };
51
+ * ```
52
+ */
53
+ export interface AndcoLock {
54
+ acquire<T>(key: string, operation: () => Promise<T>): Promise<T>;
55
+ }
56
+ /** Serializes per key within one runtime. The default, and enough for a single process. */
57
+ export declare class InProcessLock implements AndcoLock {
58
+ #private;
59
+ acquire<T>(key: string, operation: () => Promise<T>): Promise<T>;
60
+ }
61
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7D,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,2FAA2F;AAC3F,qBAAa,aAAc,YAAW,YAAY;;IAGzC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAInC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAIzC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAGrC;AAED,sGAAsG;AACtG,qBAAa,UAAW,YAAW,YAAY;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,OAAO;IAE7C,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAInC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAIzC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAGrC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAClE;AAED,2FAA2F;AAC3F,qBAAa,aAAc,YAAW,SAAS;;IAGhC,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAe9E"}
@@ -0,0 +1,47 @@
1
+ /** Keeps a session only for the lifetime of the process. The default outside a browser. */
2
+ export class MemoryStorage {
3
+ #values = new Map();
4
+ getItem(key) {
5
+ return this.#values.get(key) ?? null;
6
+ }
7
+ setItem(key, value) {
8
+ this.#values.set(key, value);
9
+ }
10
+ removeItem(key) {
11
+ this.#values.delete(key);
12
+ }
13
+ }
14
+ /** Adapts a Web Storage object. Its synchronous methods satisfy the asynchronous-capable contract. */
15
+ export class WebStorage {
16
+ storage;
17
+ constructor(storage) {
18
+ this.storage = storage;
19
+ }
20
+ getItem(key) {
21
+ return this.storage.getItem(key);
22
+ }
23
+ setItem(key, value) {
24
+ this.storage.setItem(key, value);
25
+ }
26
+ removeItem(key) {
27
+ this.storage.removeItem(key);
28
+ }
29
+ }
30
+ /** Serializes per key within one runtime. The default, and enough for a single process. */
31
+ export class InProcessLock {
32
+ #queues = new Map();
33
+ async acquire(key, operation) {
34
+ const previous = this.#queues.get(key) ?? Promise.resolve();
35
+ const run = previous.then(operation, operation);
36
+ // The stored link never rejects, so one failed operation cannot wedge the key for the next.
37
+ const link = run.then(() => undefined, () => undefined);
38
+ this.#queues.set(key, link);
39
+ try {
40
+ return await run;
41
+ }
42
+ finally {
43
+ if (this.#queues.get(key) === link)
44
+ this.#queues.delete(key);
45
+ }
46
+ }
47
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@andco/sdk",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "sideEffects": false,
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./browser": {
17
+ "types": "./dist/browser/index.d.ts",
18
+ "import": "./dist/browser/index.js",
19
+ "default": "./dist/browser/index.js"
20
+ },
21
+ "./server": {
22
+ "types": "./dist/server/index.d.ts",
23
+ "import": "./dist/server/index.js",
24
+ "default": "./dist/server/index.js"
25
+ },
26
+ "./cli": {
27
+ "types": "./dist/cli/index.d.ts",
28
+ "import": "./dist/cli/index.js",
29
+ "default": "./dist/cli/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "dependencies": {
36
+ "@andco/openapi-fetch": "0.0.1",
37
+ "@andco/protocol": "0.0.1",
38
+ "openid-client": "^6.8.8"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^24.0.0",
42
+ "typescript": "^6.0.3"
43
+ },
44
+ "license": "Apache-2.0",
45
+ "description": "Andco SDK: OAuth authorization, sessions and API access for browser and server.",
46
+ "scripts": {
47
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
48
+ "build": "pnpm run clean && tsc",
49
+ "refresh:server-metadata": "node --use-system-ca scripts/refresh-server-metadata.mjs",
50
+ "dev": "tsc --watch --preserveWatchOutput",
51
+ "check-types": "tsc --noEmit",
52
+ "lint": "tsc --noEmit",
53
+ "test": "vitest run",
54
+ "ui:check": "node -e \"\""
55
+ }
56
+ }