@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,135 @@
1
+ import type { AndCoRandomSource } from "@andco/protocol";
2
+ import { AndcoAuth } from "./auth.js";
3
+ import { type AndcoConfig, type AndcoConfigInput } from "./config.js";
4
+ import { type AndcoCredentials, type AndcoSession } from "./credentials.js";
5
+ import { AndcoIntents } from "./intents.js";
6
+ import { type AndcoAuthorizationRequest, AndcoOAuth, type AndcoTransportMode } from "./oauth.js";
7
+ import type { AndcoPresenter } from "./presenter.js";
8
+ import { AndcoRest } from "./rest.js";
9
+ import { type AndcoSessionSource, AndcoSessionStore } from "./session-store.js";
10
+ import type { AndcoLock, AndcoStorage } from "./storage.js";
11
+ /** A Resource Server Definition: immutable, and able to bind itself to a client. */
12
+ export interface AndcoResourceServer<Client> {
13
+ readonly id: string;
14
+ readonly resource: string;
15
+ /**
16
+ * Produces an operational client for this Resource Server from resolved credentials.
17
+ *
18
+ * Taking `AndcoCredentials` rather than a client is what keeps the extension point one-sided: a
19
+ * third-party Resource Server binds against one method and needs no knowledge of Andco's own
20
+ * client shape, transport, or configuration.
21
+ */
22
+ use(credentials: AndcoCredentials): Client;
23
+ }
24
+ /** Platform capabilities the SDK reads off the runtime, grouped so a future addition is not a breaking change. */
25
+ export type AndcoGlobals = {
26
+ fetch: typeof globalThis.fetch;
27
+ /** Threaded into the Protocol Contract's `RANDOM_UUID`, so a runtime without Web Crypto can supply its own. */
28
+ crypto: AndCoRandomSource;
29
+ };
30
+ export type AndcoClientOptions = AndcoConfigInput & {
31
+ /** Confidential credential. Its presence is what makes this a confidential OAuth client. */
32
+ clientSecret?: string | null;
33
+ globals?: Partial<AndcoGlobals>;
34
+ /** Authorization Transport Policy. Defaults to `auto`. */
35
+ transport?: AndcoTransportMode;
36
+ /** Presents an authorization to the user. Omit on a runtime that cannot present one. */
37
+ presenter?: AndcoPresenter;
38
+ /** Persists a transaction so it survives a full-page redirect. */
39
+ persistTransaction?: (request: AndcoAuthorizationRequest) => void | Promise<void>;
40
+ storage?: AndcoStorage;
41
+ /** A session the caller already resolved, so the first snapshot is real rather than unresolved. */
42
+ initialSession?: AndcoSession | null;
43
+ lock?: AndcoLock;
44
+ /** Consulted once during the load, before storage. This is where a URL callback enters. */
45
+ source?: AndcoSessionSource;
46
+ persist?: boolean;
47
+ };
48
+ /**
49
+ * An Andco Instance without resolved credentials.
50
+ *
51
+ * This is a value: configuration plus capability, built synchronously, running no timers of its
52
+ * own. `rest` and `intents` reach the API through `session`, which answers with no token until a
53
+ * session exists — exactly what public resources such as the Permission Catalog want, and exactly
54
+ * what every other resource will reject with a 401. `with(credentials)` produces the authorized
55
+ * arm of the same shape for a Profile, Company, or Grant.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * export const andco = new AndcoClient({ clientId, clientSecret });
60
+ *
61
+ * // public, no credentials involved
62
+ * const { data } = await andco.rest.http.GET("/catalog/permissions");
63
+ *
64
+ * // per request, credentials have the lifetime of the request that carried them
65
+ * const bound = andco.with(await sessionFromCookie());
66
+ * ```
67
+ */
68
+ export declare class AndcoClient {
69
+ readonly config: AndcoConfig;
70
+ readonly clientSecret: string | null;
71
+ readonly globals: AndcoGlobals;
72
+ readonly oauth: AndcoOAuth;
73
+ readonly rest: AndcoRest;
74
+ readonly intents: AndcoIntents;
75
+ readonly auth: AndcoAuth;
76
+ readonly session: AndcoSessionStore;
77
+ /** Runtime discriminant: `false` here, `true` on the value `with(...)` returns. */
78
+ readonly isAuthed: false;
79
+ constructor(options: AndcoClientOptions);
80
+ /**
81
+ * Binds *explicit* credentials for one unit of work, owning its own REST surface.
82
+ *
83
+ * Accepts a bare access token, a session, a session store, or anything that resolves a token.
84
+ * The bare token exists because it is the commonest server shape by far — a handler that already
85
+ * pulled one out of its own session — and requiring a wrapper object there put the same three
86
+ * lines at the top of every route.
87
+ *
88
+ * The result carries its own in-memory session store, so `auth` has something to operate on: its
89
+ * sign-in fails with the browser-required condition, because a token-bound client presents
90
+ * nothing, and its sign-out clears that memory harmlessly. `accessTokenFor` always answers with
91
+ * the credentials given here, never with anything that store might hold, and `rest`/`intents` are
92
+ * new objects scoped to those credentials — deliberately not shared with the instance, since a
93
+ * server binds different credentials on every request. For the instance's *own* session, reach
94
+ * for `authorized()` instead.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * const bound = andco.with(await accessTokenFromCookie(request.headers));
99
+ * const { data } = await bound.rest.http.GET("/accounts");
100
+ * ```
101
+ */
102
+ with(credentials: AndcoCredentials | AndcoSession | string): AndcoClientAuthed;
103
+ /**
104
+ * The instance's own session, seen as authorized — a new reference every call, sharing every
105
+ * collaborator with the instance by reference.
106
+ *
107
+ * This is the seam a framework adapter reaches for: identity is the change signal it re-renders
108
+ * on, so it needs a fresh object per Authorized Identity, yet `rest`, `intents`, `auth`, and
109
+ * `session` must stay exactly what they already were, or anything an application memoised against
110
+ * them stops working the moment it signs in. Unlike `with(...)`, `accessTokenFor` here delegates
111
+ * to the session store on every call, so a rotated token is still picked up — it is never bound to
112
+ * the snapshot present when `authorized()` was called.
113
+ *
114
+ * Constructed honestly, with no `Object.create` or prototype trick: a private field added to
115
+ * `AndcoClient` later cannot silently break this.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * const andco = session ? instance.authorized() : instance;
120
+ * ```
121
+ */
122
+ authorized(): AndcoClientAuthed;
123
+ static resolveCredentials(value: AndcoCredentials | AndcoSession | string): AndcoCredentials;
124
+ }
125
+ /**
126
+ * The same shape as {@link AndcoClient}, plus resolved credentials and `isAuthed` fixed to `true`.
127
+ *
128
+ * The split is informational, not enforced: both arms expose the same `rest` and `intents`, and a
129
+ * protected call without a session is still a runtime 401, not a compile error. Enforcing it would
130
+ * need two typed views of the API contract, which was considered and rejected.
131
+ */
132
+ export type AndcoClientAuthed = Omit<AndcoClient, "isAuthed" | "with" | "authorized"> & AndcoCredentials & {
133
+ readonly isAuthed: true;
134
+ };
135
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js ADDED
@@ -0,0 +1,210 @@
1
+ import { AndcoAuth } from "./auth.js";
2
+ import { AndcoConfig as Config } from "./config.js";
3
+ import { credentialsFromSession, isCredentials } from "./credentials.js";
4
+ import { Result } from "./errors.js";
5
+ import { AndcoIntents } from "./intents.js";
6
+ import { AndcoOAuth } from "./oauth.js";
7
+ import { AndcoRest } from "./rest.js";
8
+ import { AndcoSessionStore, browserRequired } from "./session-store.js";
9
+ /**
10
+ * An Andco Instance without resolved credentials.
11
+ *
12
+ * This is a value: configuration plus capability, built synchronously, running no timers of its
13
+ * own. `rest` and `intents` reach the API through `session`, which answers with no token until a
14
+ * session exists — exactly what public resources such as the Permission Catalog want, and exactly
15
+ * what every other resource will reject with a 401. `with(credentials)` produces the authorized
16
+ * arm of the same shape for a Profile, Company, or Grant.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * export const andco = new AndcoClient({ clientId, clientSecret });
21
+ *
22
+ * // public, no credentials involved
23
+ * const { data } = await andco.rest.http.GET("/catalog/permissions");
24
+ *
25
+ * // per request, credentials have the lifetime of the request that carried them
26
+ * const bound = andco.with(await sessionFromCookie());
27
+ * ```
28
+ */
29
+ export class AndcoClient {
30
+ config;
31
+ clientSecret;
32
+ globals;
33
+ oauth;
34
+ rest;
35
+ intents;
36
+ auth;
37
+ session;
38
+ /** Runtime discriminant: `false` here, `true` on the value `with(...)` returns. */
39
+ isAuthed = false;
40
+ constructor(options) {
41
+ this.config = Config.from(options);
42
+ this.clientSecret = options.clientSecret ?? null;
43
+ this.globals = {
44
+ fetch: options.globals?.fetch ?? globalThis.fetch.bind(globalThis),
45
+ crypto: options.globals?.crypto ?? globalThis.crypto,
46
+ };
47
+ this.oauth = new AndcoOAuth({
48
+ config: this.config,
49
+ fetch: this.globals.fetch,
50
+ clientSecret: this.clientSecret,
51
+ transport: options.transport,
52
+ });
53
+ // The store is real even without a presenter: it is what lets `rest` stay one object across a
54
+ // sign-in, and what gives `auth` something to operate on regardless of runtime.
55
+ this.session = new AndcoSessionStore({
56
+ config: this.config,
57
+ oauth: this.oauth,
58
+ storage: options.storage,
59
+ initialSession: options.initialSession,
60
+ lock: options.lock,
61
+ source: options.source,
62
+ persist: options.persist,
63
+ });
64
+ this.auth = new AndcoAuth({
65
+ oauth: this.oauth,
66
+ store: this.session,
67
+ presenter: options.presenter ?? statelessPresenter(),
68
+ persistTransaction: options.persistTransaction,
69
+ crypto: this.globals.crypto,
70
+ });
71
+ this.rest = new AndcoRest({ config: this.config, fetch: this.globals.fetch, credentials: this.session });
72
+ this.intents = new AndcoIntents(this.rest, this.globals.crypto);
73
+ }
74
+ /**
75
+ * Binds *explicit* credentials for one unit of work, owning its own REST surface.
76
+ *
77
+ * Accepts a bare access token, a session, a session store, or anything that resolves a token.
78
+ * The bare token exists because it is the commonest server shape by far — a handler that already
79
+ * pulled one out of its own session — and requiring a wrapper object there put the same three
80
+ * lines at the top of every route.
81
+ *
82
+ * The result carries its own in-memory session store, so `auth` has something to operate on: its
83
+ * sign-in fails with the browser-required condition, because a token-bound client presents
84
+ * nothing, and its sign-out clears that memory harmlessly. `accessTokenFor` always answers with
85
+ * the credentials given here, never with anything that store might hold, and `rest`/`intents` are
86
+ * new objects scoped to those credentials — deliberately not shared with the instance, since a
87
+ * server binds different credentials on every request. For the instance's *own* session, reach
88
+ * for `authorized()` instead.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * const bound = andco.with(await accessTokenFromCookie(request.headers));
93
+ * const { data } = await bound.rest.http.GET("/accounts");
94
+ * ```
95
+ */
96
+ with(credentials) {
97
+ return new AndcoAuthedClientImpl(this, AndcoClient.resolveCredentials(credentials));
98
+ }
99
+ /**
100
+ * The instance's own session, seen as authorized — a new reference every call, sharing every
101
+ * collaborator with the instance by reference.
102
+ *
103
+ * This is the seam a framework adapter reaches for: identity is the change signal it re-renders
104
+ * on, so it needs a fresh object per Authorized Identity, yet `rest`, `intents`, `auth`, and
105
+ * `session` must stay exactly what they already were, or anything an application memoised against
106
+ * them stops working the moment it signs in. Unlike `with(...)`, `accessTokenFor` here delegates
107
+ * to the session store on every call, so a rotated token is still picked up — it is never bound to
108
+ * the snapshot present when `authorized()` was called.
109
+ *
110
+ * Constructed honestly, with no `Object.create` or prototype trick: a private field added to
111
+ * `AndcoClient` later cannot silently break this.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const andco = session ? instance.authorized() : instance;
116
+ * ```
117
+ */
118
+ authorized() {
119
+ return new AndcoAuthorizedViewImpl(this);
120
+ }
121
+ static resolveCredentials(value) {
122
+ if (typeof value === "string") {
123
+ return { accessTokenFor: async () => value };
124
+ }
125
+ else if (isCredentials(value)) {
126
+ return value;
127
+ }
128
+ else {
129
+ return credentialsFromSession(value);
130
+ }
131
+ }
132
+ }
133
+ /** Presents nothing; every authorization attempt fails clearly instead of doing nothing silently. */
134
+ function statelessPresenter() {
135
+ return {
136
+ async present() {
137
+ return Result.fail(browserRequired());
138
+ },
139
+ };
140
+ }
141
+ /**
142
+ * The implementation behind `with(...)`. Not exported: `AndcoClientAuthed` is the entire public
143
+ * contract, so a caller never has a reason to name this type.
144
+ */
145
+ class AndcoAuthedClientImpl {
146
+ credentials;
147
+ isAuthed = true;
148
+ config;
149
+ clientSecret;
150
+ globals;
151
+ oauth;
152
+ rest;
153
+ intents;
154
+ auth;
155
+ session;
156
+ constructor(owner, credentials) {
157
+ this.credentials = credentials;
158
+ this.config = owner.config;
159
+ this.clientSecret = owner.clientSecret;
160
+ this.globals = owner.globals;
161
+ this.oauth = owner.oauth;
162
+ this.rest = new AndcoRest({ config: owner.config, fetch: owner.globals.fetch, credentials });
163
+ this.intents = new AndcoIntents(this.rest, owner.globals.crypto);
164
+ // In-memory and unlinked from `credentials`: it exists only so `auth` has something to operate
165
+ // on, since a bare token or a caller-supplied resolver carries no persistable session shape.
166
+ this.session = new AndcoSessionStore({ config: owner.config, oauth: owner.oauth, persist: false });
167
+ this.auth = new AndcoAuth({
168
+ oauth: owner.oauth,
169
+ store: this.session,
170
+ presenter: statelessPresenter(),
171
+ crypto: owner.globals.crypto,
172
+ });
173
+ }
174
+ accessTokenFor(resource) {
175
+ return this.credentials.accessTokenFor(resource);
176
+ }
177
+ }
178
+ /**
179
+ * The implementation behind `authorized()`. A genuinely new object, but every collaborator field is
180
+ * the instance's own by reference — that is what keeps `rest`, `intents`, `auth`, and `session`
181
+ * unchanged across a sign-in, satisfying "one REST surface per instance" even where a framework
182
+ * adapter hands out a fresh `AndcoClientAuthed` reference on every Authorized Identity change.
183
+ */
184
+ class AndcoAuthorizedViewImpl {
185
+ owner;
186
+ isAuthed = true;
187
+ config;
188
+ clientSecret;
189
+ globals;
190
+ oauth;
191
+ rest;
192
+ intents;
193
+ auth;
194
+ session;
195
+ constructor(owner) {
196
+ this.owner = owner;
197
+ this.config = owner.config;
198
+ this.clientSecret = owner.clientSecret;
199
+ this.globals = owner.globals;
200
+ this.oauth = owner.oauth;
201
+ this.rest = owner.rest;
202
+ this.intents = owner.intents;
203
+ this.auth = owner.auth;
204
+ this.session = owner.session;
205
+ }
206
+ /** Delegates to the instance's own session store on every call, so a rotated token is still picked up. */
207
+ accessTokenFor(resource) {
208
+ return this.owner.session.accessTokenFor(resource);
209
+ }
210
+ }
@@ -0,0 +1,82 @@
1
+ import { type AndCoScope, type AndCoThemeMode } from "@andco/protocol";
2
+ /** Visual theme available for hosted Andco surfaces. */
3
+ export type AndcoTheme = "base";
4
+ /**
5
+ * An OAuth scope. Known Andco scopes are suggested; a Resource Server may define its own.
6
+ *
7
+ * The Protocol Contract already knows the scope vocabulary, so this is that type rather than a bare
8
+ * `string`: an editor completes `bank_accounts:read` without closing the door on a scope Andco has
9
+ * not shipped yet.
10
+ */
11
+ export type AndcoScope = AndCoScope;
12
+ /** Everything an Andco Instance needs to know that is not a credential. */
13
+ export type AndcoConfigInput = {
14
+ /** OAuth `client_id` registered in Andco. */
15
+ clientId: string;
16
+ /** Supabase Auth-compatible OAuth issuer base URL. */
17
+ endpointAuth?: string | URL;
18
+ /** Intent Transport origin: the Andco resource API, or a same-origin Project Backend. */
19
+ endpointApi?: string | URL;
20
+ /** Origin serving hosted Andco surfaces. */
21
+ endpointWidget?: string | URL;
22
+ /** Resource API contract version. A ten-digit Unix timestamp, or `_` for the latest. */
23
+ apiVersion?: string | number;
24
+ /** Registered OAuth `redirect_uri` for this client. */
25
+ redirectTo?: string | URL;
26
+ /** BCP 47 locale used by hosted Andco surfaces. */
27
+ locale?: string;
28
+ theme?: AndcoTheme;
29
+ themeMode?: AndCoThemeMode;
30
+ /** Scopes requested when a request does not name its own. */
31
+ initialScopes?: readonly AndcoScope[];
32
+ /** Additional exact origins allowed to receive or send popup callback messages. */
33
+ allowedPopupOrigins?: readonly string[];
34
+ };
35
+ /**
36
+ * The resolved, validated configuration shared by every collaborator of one Andco Instance.
37
+ *
38
+ * Configuration used to be declared two and three times across a client, a callback consumer, and a
39
+ * framework provider, which is how an application ended up with two copies that could disagree.
40
+ * This is normalized once and read from the instance, never passed again.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const config = AndcoConfig.from({ clientId: "client_123", endpointApi: "https://bank.localhost" });
45
+ * const tokenEndpoint = config.authURL("oauth/token");
46
+ * ```
47
+ */
48
+ export declare class AndcoConfig {
49
+ readonly clientId: string;
50
+ readonly endpoints: Readonly<{
51
+ auth: URL;
52
+ api: URL;
53
+ widget: URL;
54
+ }>;
55
+ readonly apiVersion: string;
56
+ readonly redirectTo: URL | undefined;
57
+ readonly locale: string;
58
+ readonly theme: AndcoTheme;
59
+ readonly themeMode: AndCoThemeMode;
60
+ readonly initialScopes: readonly AndcoScope[];
61
+ readonly allowedPopupOrigins: ReadonlySet<string>;
62
+ private constructor();
63
+ /** Normalizes and validates one configuration. Throws, because a bad value is a startup defect. */
64
+ static from(input: AndcoConfigInput): AndcoConfig;
65
+ /** Derives a variant without mutating this one. */
66
+ with(overrides: Partial<AndcoConfigInput>): AndcoConfig;
67
+ /**
68
+ * Builds a URL under the Authorization Server, so no caller re-derives one by string-joining an
69
+ * origin with a path. Both example backends did exactly that before this existed.
70
+ */
71
+ authURL(path: string): URL;
72
+ /** Base URL of the versioned OAuth resource API. */
73
+ apiURL(): URL;
74
+ /** Exact immutable URL of a hosted Andco surface for one release. */
75
+ widgetURL(release: number, surface?: "button"): URL;
76
+ /** Whether an origin may participate in popup messaging for this instance. */
77
+ allowsPopupOrigin(origin: string): boolean;
78
+ toJSON(): AndcoConfigInput;
79
+ }
80
+ /** Loopback over plain HTTP is accepted so local development and CLI callbacks work unmodified. */
81
+ export declare function isLoopback(url: URL): boolean;
82
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AAGzB,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC;AAChC;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC;AAEpC,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAC5B,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAC3B,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAC9B,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAC1B,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,6DAA6D;IAC7D,aAAa,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,mFAAmF;IACnF,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC,CAAC;AAMF;;;;;;;;;;;;GAYG;AACH,qBAAa,WAAW;IACtB,SAAgB,QAAQ,EAAE,MAAM,CAAC;IACjC,SAAgB,SAAS,EAAE,QAAQ,CAAC;QAAE,IAAI,EAAE,GAAG,CAAC;QAAC,GAAG,EAAE,GAAG,CAAC;QAAC,MAAM,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;IAC1E,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,UAAU,EAAE,GAAG,GAAG,SAAS,CAAC;IAC5C,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,SAAgB,KAAK,EAAE,UAAU,CAAC;IAClC,SAAgB,SAAS,EAAE,cAAc,CAAC;IAC1C,SAAgB,aAAa,EAAE,SAAS,UAAU,EAAE,CAAC;IACrD,SAAgB,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAEzD,OAAO;IAkCP,mGAAmG;WACrF,IAAI,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW;IAIxD,mDAAmD;IAC5C,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,WAAW;IAI9D;;;OAGG;IACI,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG;IAOjC,oDAAoD;IAC7C,MAAM,IAAI,GAAG;IAIpB,qEAAqE;IAC9D,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,QAAmB,GAAG,GAAG;IAIpE,8EAA8E;IACvE,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAI1C,MAAM,IAAI,gBAAgB;CAelC;AAED,mGAAmG;AACnG,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAQ5C"}
package/dist/config.js ADDED
@@ -0,0 +1,109 @@
1
+ import { ANDCO_API_ORIGIN, ANDCO_AUTH_ORIGIN, ANDCO_WIDGET_ORIGIN, SafeURL, } from "@andco/protocol";
2
+ import { ANDCO_ERROR_CODES, AndcoError } from "./errors.js";
3
+ const LOCALE_PATTERN = /^[A-Za-z]{2,8}(-[A-Za-z0-9]{2,8})*$/;
4
+ const CLIENT_ID_PATTERN = /^[A-Za-z0-9_-]{1,80}$/;
5
+ const API_VERSION_PATTERN = /^\d{10}$/;
6
+ /**
7
+ * The resolved, validated configuration shared by every collaborator of one Andco Instance.
8
+ *
9
+ * Configuration used to be declared two and three times across a client, a callback consumer, and a
10
+ * framework provider, which is how an application ended up with two copies that could disagree.
11
+ * This is normalized once and read from the instance, never passed again.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const config = AndcoConfig.from({ clientId: "client_123", endpointApi: "https://bank.localhost" });
16
+ * const tokenEndpoint = config.authURL("oauth/token");
17
+ * ```
18
+ */
19
+ export class AndcoConfig {
20
+ clientId;
21
+ endpoints;
22
+ apiVersion;
23
+ redirectTo;
24
+ locale;
25
+ theme;
26
+ themeMode;
27
+ initialScopes;
28
+ allowedPopupOrigins;
29
+ constructor(input) {
30
+ if (!CLIENT_ID_PATTERN.test(input.clientId ?? "")) {
31
+ throw new AndcoError(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, { message: "clientId is missing or malformed" });
32
+ }
33
+ this.clientId = input.clientId;
34
+ this.endpoints = Object.freeze({
35
+ auth: new SafeURL(input.endpointAuth ?? ANDCO_AUTH_ORIGIN),
36
+ api: new SafeURL(input.endpointApi ?? ANDCO_API_ORIGIN),
37
+ widget: new SafeURL(input.endpointWidget ?? ANDCO_WIDGET_ORIGIN),
38
+ });
39
+ this.apiVersion = String(input.apiVersion ?? "_");
40
+ if (this.apiVersion !== "_" && !API_VERSION_PATTERN.test(this.apiVersion)) {
41
+ throw new AndcoError(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, {
42
+ message: "apiVersion must be `_` or a ten-digit Unix timestamp",
43
+ });
44
+ }
45
+ this.redirectTo = input.redirectTo === undefined ? undefined : new SafeURL(input.redirectTo);
46
+ this.locale = input.locale ?? "es";
47
+ if (!LOCALE_PATTERN.test(this.locale)) {
48
+ throw new AndcoError(ANDCO_ERROR_CODES.INVALID_CONFIGURATION, { message: "locale must be a BCP 47 tag" });
49
+ }
50
+ this.theme = input.theme ?? "base";
51
+ this.themeMode = input.themeMode ?? "system";
52
+ this.initialScopes = Object.freeze([...new Set(input.initialScopes ?? [])]);
53
+ const incoming = input.allowedPopupOrigins ?? [];
54
+ this.allowedPopupOrigins = Object.freeze(new Set([this.endpoints.widget.origin, ...incoming.map((o) => new SafeURL(o).origin)]));
55
+ }
56
+ /** Normalizes and validates one configuration. Throws, because a bad value is a startup defect. */
57
+ static from(input) {
58
+ return new AndcoConfig(input);
59
+ }
60
+ /** Derives a variant without mutating this one. */
61
+ with(overrides) {
62
+ return AndcoConfig.from({ ...this.toJSON(), ...overrides });
63
+ }
64
+ /**
65
+ * Builds a URL under the Authorization Server, so no caller re-derives one by string-joining an
66
+ * origin with a path. Both example backends did exactly that before this existed.
67
+ */
68
+ authURL(path) {
69
+ const base = this.endpoints.auth;
70
+ const url = new URL(base.href);
71
+ url.pathname = `${base.pathname.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
72
+ return url;
73
+ }
74
+ /** Base URL of the versioned OAuth resource API. */
75
+ apiURL() {
76
+ return new URL(`/oauth/api/versions/${this.apiVersion}/`, this.endpoints.api);
77
+ }
78
+ /** Exact immutable URL of a hosted Andco surface for one release. */
79
+ widgetURL(release, surface = "button") {
80
+ return new URL(`/embedded/versions/${release}/${surface}`, this.endpoints.widget);
81
+ }
82
+ /** Whether an origin may participate in popup messaging for this instance. */
83
+ allowsPopupOrigin(origin) {
84
+ return this.allowedPopupOrigins.has(origin);
85
+ }
86
+ toJSON() {
87
+ return {
88
+ clientId: this.clientId,
89
+ endpointAuth: this.endpoints.auth.href,
90
+ endpointApi: this.endpoints.api.href,
91
+ endpointWidget: this.endpoints.widget.href,
92
+ apiVersion: this.apiVersion,
93
+ redirectTo: this.redirectTo?.href,
94
+ locale: this.locale,
95
+ theme: this.theme,
96
+ themeMode: this.themeMode,
97
+ initialScopes: [...this.initialScopes],
98
+ allowedPopupOrigins: [...this.allowedPopupOrigins],
99
+ };
100
+ }
101
+ }
102
+ /** Loopback over plain HTTP is accepted so local development and CLI callbacks work unmodified. */
103
+ export function isLoopback(url) {
104
+ return (url.hostname === "localhost" ||
105
+ url.hostname === "127.0.0.1" ||
106
+ url.hostname === "[::1]" ||
107
+ url.hostname === "::1" ||
108
+ url.hostname.endsWith(".localhost"));
109
+ }
@@ -0,0 +1,76 @@
1
+ /** The authenticated subject of a session, as far as the SDK is concerned. */
2
+ export type AndcoUser = {
3
+ id: string;
4
+ name: string | null;
5
+ email: string | null;
6
+ avatarUrl: string | null;
7
+ };
8
+ /**
9
+ * One set of credentials issued by Andco.
10
+ *
11
+ * `resourceAccessTokens` exists because RFC 8707 gives each Resource Server its own audience. The
12
+ * Authorization Server currently returns one token tuple, so today every entry holds the same
13
+ * token; when per-audience issuance arrives, nothing in the SDK's signatures changes.
14
+ */
15
+ export type AndcoSession = {
16
+ accessToken: string;
17
+ refreshToken: string | null;
18
+ tokenType: string;
19
+ /** Absolute expiry in seconds since the Unix epoch. */
20
+ expiresAt: number;
21
+ scopes: readonly string[];
22
+ user: AndcoUser;
23
+ /** Access tokens keyed by exact Resource Indicator URI. Absent means `accessToken` serves all. */
24
+ resourceAccessTokens?: Readonly<Record<string, string>>;
25
+ };
26
+ /**
27
+ * The entire contract the Andco core knows about authorization.
28
+ *
29
+ * Keeping it to one resource-keyed method is what lets an application go on owning its sessions:
30
+ * a Better Auth account, a Passport session, a row in your own table, or a plain closure all
31
+ * satisfy it without adopting the SDK's persistence.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const bound = andco.with({
36
+ * accessTokenFor: async () => request.session.andcoAccessToken,
37
+ * });
38
+ * ```
39
+ */
40
+ export interface AndcoCredentials {
41
+ /** Resolves a token for one Resource Indicator, refreshing lazily when the current one is stale. */
42
+ accessTokenFor(resource: string): Promise<string | null>;
43
+ }
44
+ /** Seconds of remaining lifetime under which a token is treated as stale and refreshed. */
45
+ export declare const ANDCO_REFRESH_SKEW_SECONDS = 30;
46
+ /** Whether a session should be refreshed before its token is handed out. */
47
+ export declare function isExpired(session: AndcoSession, nowSeconds?: number): boolean;
48
+ /** Reads the token a session holds for one Resource Indicator, without refreshing. */
49
+ export declare function tokenFor(session: AndcoSession, resource: string): string | null;
50
+ /**
51
+ * Adapts a plain session into the credential contract, without persistence or refresh.
52
+ *
53
+ * This is the per-request shape on a server: the caller already resolved the credentials for this
54
+ * request and their lifetime is the request's.
55
+ */
56
+ export declare function credentialsFromSession(session: AndcoSession): AndcoCredentials;
57
+ /** Whether a value already satisfies the credential contract. */
58
+ export declare function isCredentials(value: AndcoCredentials | AndcoSession): value is AndcoCredentials;
59
+ /**
60
+ * Identifies *who* a session authorizes and *what* they granted, ignoring which token carries it.
61
+ *
62
+ * A refresh publishes a whole new session object several times a day, so reacting to session
63
+ * identity re-runs an application's effects on a rotation that changed nothing it can observe —
64
+ * tokens are resolved per request, never held. This narrows the signal to the two things an
65
+ * application does react to: the signed-in subject, and the scopes it may exercise.
66
+ *
67
+ * Its three unresolved states stay distinct, because rendering "loading" as "signed out" is the
68
+ * flash `useAndcoSession` exists to avoid.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * useEffect(() => void refetch(), [sessionIdentityKey(session)]);
73
+ * ```
74
+ */
75
+ export declare function sessionIdentityKey(session: AndcoSession | null | undefined): string;
76
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,kGAAkG;IAClG,oBAAoB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACzD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oGAAoG;IACpG,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,2FAA2F;AAC3F,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C,4EAA4E;AAC5E,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,SAAgC,GAAG,OAAO,CAEpG;AAED,sFAAsF;AACtF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY/E;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,YAAY,GAAG,gBAAgB,CAE9E;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,YAAY,GAAG,KAAK,IAAI,gBAAgB,CAE/F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAMnF"}
Binary file