@mirrorstack-ai/app-module-client 0.5.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.
- package/CHANGELOG.md +152 -0
- package/LICENSE +202 -0
- package/README.md +488 -0
- package/dist/base-url.d.ts +39 -0
- package/dist/base-url.js +51 -0
- package/dist/base-url.js.map +1 -0
- package/dist/client.d.ts +56 -0
- package/dist/client.js +85 -0
- package/dist/client.js.map +1 -0
- package/dist/error.d.ts +29 -0
- package/dist/error.js +100 -0
- package/dist/error.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/next/auth-routes.d.ts +82 -0
- package/dist/next/auth-routes.js +164 -0
- package/dist/next/auth-routes.js.map +1 -0
- package/dist/next/index.d.ts +12 -0
- package/dist/next/index.js +13 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/module-proxy-routes.d.ts +81 -0
- package/dist/next/module-proxy-routes.js +77 -0
- package/dist/next/module-proxy-routes.js.map +1 -0
- package/dist/plugin.d.ts +39 -0
- package/dist/plugin.js +39 -0
- package/dist/plugin.js.map +1 -0
- package/dist/response.d.ts +13 -0
- package/dist/response.js +80 -0
- package/dist/response.js.map +1 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/member-sessions.d.ts +80 -0
- package/dist/server/member-sessions.js +162 -0
- package/dist/server/member-sessions.js.map +1 -0
- package/dist/transport.d.ts +98 -0
- package/dist/transport.js +299 -0
- package/dist/transport.js.map +1 -0
- package/dist/web/cache.d.ts +33 -0
- package/dist/web/cache.js +130 -0
- package/dist/web/cache.js.map +1 -0
- package/dist/web/component-mount.d.ts +23 -0
- package/dist/web/component-mount.js +111 -0
- package/dist/web/component-mount.js.map +1 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +7 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/localized-text.d.ts +10 -0
- package/dist/web/localized-text.js +36 -0
- package/dist/web/localized-text.js.map +1 -0
- package/dist/web/react.d.ts +18 -0
- package/dist/web/react.js +111 -0
- package/dist/web/react.js.map +1 -0
- package/dist/web/runtime.d.ts +60 -0
- package/dist/web/runtime.js +73 -0
- package/dist/web/runtime.js.map +1 -0
- package/dist/web/subpath.d.ts +22 -0
- package/dist/web/subpath.js +50 -0
- package/dist/web/subpath.js.map +1 -0
- package/dist/web/types.d.ts +129 -0
- package/dist/web/types.js +2 -0
- package/dist/web/types.js.map +1 -0
- package/dist/web/use-now.d.ts +7 -0
- package/dist/web/use-now.js +22 -0
- package/dist/web/use-now.js.map +1 -0
- package/dist/web/use-platform-unsaved-state.d.ts +10 -0
- package/dist/web/use-platform-unsaved-state.js +35 -0
- package/dist/web/use-platform-unsaved-state.js.map +1 -0
- package/package.json +88 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { MODULE_CLIENT_API_VERSION, assertModuleRef, } from "./plugin.js";
|
|
2
|
+
import { resolveMaxResponseBytes } from "./response.js";
|
|
3
|
+
import { createScopedTransports, } from "./transport.js";
|
|
4
|
+
function resolveFetch(fetchImplementation) {
|
|
5
|
+
if (fetchImplementation !== undefined) {
|
|
6
|
+
if (typeof fetchImplementation !== "function") {
|
|
7
|
+
throw new TypeError("fetch must be a function");
|
|
8
|
+
}
|
|
9
|
+
return fetchImplementation;
|
|
10
|
+
}
|
|
11
|
+
if (typeof globalThis.fetch !== "function") {
|
|
12
|
+
throw new TypeError("globalThis.fetch is unavailable; provide a fetch implementation");
|
|
13
|
+
}
|
|
14
|
+
return globalThis.fetch.bind(globalThis);
|
|
15
|
+
}
|
|
16
|
+
function validatePlatformAuth(platformAuth) {
|
|
17
|
+
if (platformAuth === undefined)
|
|
18
|
+
return;
|
|
19
|
+
if (platformAuth === null || typeof platformAuth !== "object") {
|
|
20
|
+
throw new TypeError("platformAuth must be an object");
|
|
21
|
+
}
|
|
22
|
+
if (typeof platformAuth.getAccessToken !== "function") {
|
|
23
|
+
throw new TypeError("platformAuth.getAccessToken must be a function");
|
|
24
|
+
}
|
|
25
|
+
if (platformAuth.refreshAccessToken !== undefined &&
|
|
26
|
+
typeof platformAuth.refreshAccessToken !== "function") {
|
|
27
|
+
throw new TypeError("platformAuth.refreshAccessToken must be a function");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates an application client from an explicit object of module plugins.
|
|
32
|
+
*
|
|
33
|
+
* The object keys are local aliases and do not affect routing. A module
|
|
34
|
+
* reference may occur only once so two aliases cannot silently address the
|
|
35
|
+
* same installed module with different expectations.
|
|
36
|
+
*/
|
|
37
|
+
export function createAppClient(options) {
|
|
38
|
+
if (options === null || typeof options !== "object") {
|
|
39
|
+
throw new TypeError("application client options must be an object");
|
|
40
|
+
}
|
|
41
|
+
if (options.modules === null || typeof options.modules !== "object") {
|
|
42
|
+
throw new TypeError("modules must be an object of module plugins");
|
|
43
|
+
}
|
|
44
|
+
validatePlatformAuth(options.platformAuth);
|
|
45
|
+
const entries = Object.entries(options.modules);
|
|
46
|
+
const refs = new Set();
|
|
47
|
+
for (const [alias, plugin] of entries) {
|
|
48
|
+
if (plugin === null || typeof plugin !== "object") {
|
|
49
|
+
throw new TypeError(`module ${alias} is not a module client plugin`);
|
|
50
|
+
}
|
|
51
|
+
if (plugin.apiVersion !== MODULE_CLIENT_API_VERSION) {
|
|
52
|
+
throw new TypeError(`module ${alias} uses unsupported client API version ${String(plugin.apiVersion)}`);
|
|
53
|
+
}
|
|
54
|
+
assertModuleRef(plugin.moduleRef);
|
|
55
|
+
if (typeof plugin.create !== "function") {
|
|
56
|
+
throw new TypeError(`module ${alias} does not provide a create function`);
|
|
57
|
+
}
|
|
58
|
+
if (refs.has(plugin.moduleRef)) {
|
|
59
|
+
throw new TypeError(`duplicate moduleRef registration: ${plugin.moduleRef}`);
|
|
60
|
+
}
|
|
61
|
+
refs.add(plugin.moduleRef);
|
|
62
|
+
}
|
|
63
|
+
const shared = {
|
|
64
|
+
baseUrl: options.baseUrl,
|
|
65
|
+
fetch: resolveFetch(options.fetch),
|
|
66
|
+
credentials: options.credentials ?? "include",
|
|
67
|
+
maxResponseBytes: resolveMaxResponseBytes(options.maxResponseBytes),
|
|
68
|
+
...(options.headers === undefined ? {} : { headers: options.headers }),
|
|
69
|
+
...(options.memberCredential === undefined ? {} : { memberCredential: options.memberCredential }),
|
|
70
|
+
...(options.metadata === undefined ? {} : { metadata: options.metadata }),
|
|
71
|
+
...(options.platformAuth === undefined ? {} : { platformAuth: options.platformAuth }),
|
|
72
|
+
};
|
|
73
|
+
const modules = {};
|
|
74
|
+
for (const [alias, plugin] of entries) {
|
|
75
|
+
const api = plugin.create(createScopedTransports(shared, plugin.moduleRef));
|
|
76
|
+
Object.defineProperty(modules, alias, {
|
|
77
|
+
configurable: false,
|
|
78
|
+
enumerable: true,
|
|
79
|
+
value: api,
|
|
80
|
+
writable: false,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return Object.freeze({ modules: Object.freeze(modules) });
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n MODULE_CLIENT_API_VERSION,\n assertModuleRef,\n type ModuleClientPlugin,\n} from \"./plugin.js\";\nimport { resolveMaxResponseBytes } from \"./response.js\";\nimport {\n createScopedTransports,\n type PlatformAuth,\n type RequestHeaders,\n type RequestMetadata,\n type SharedTransportConfig,\n} from \"./transport.js\";\n\n/** A named set of explicitly imported module plugins. */\nexport type ModulePluginMap = Readonly<Record<string, ModuleClientPlugin<unknown>>>;\n\n/** Resolves the API created by one module plugin. */\nexport type ModuleApi<TPlugin> =\n TPlugin extends ModuleClientPlugin<infer TApi> ? TApi : never;\n\n/** The immutable application client returned by {@link createAppClient}. */\nexport interface AppClient<TModules extends ModulePluginMap> {\n /** Typed APIs keyed by the aliases supplied in `modules`. */\n readonly modules: Readonly<{\n [TKey in keyof TModules]: ModuleApi<TModules[TKey]>;\n }>;\n}\n\n/** Configuration for {@link createAppClient}. */\nexport interface CreateAppClientOptions<TModules extends ModulePluginMap> {\n /**\n * App dispatch root; the client appends `/<scope>/<moduleRef>/<path>`.\n * Use `platformBaseUrl(...)` (`https://api.<org-domain>/v1/apps/app/<appSlug>`)\n * for a direct platform connection, or a same-origin BFF path.\n */\n readonly baseUrl: string;\n /** Explicit plugin composition; no package is discovered dynamically. */\n readonly modules: TModules;\n /** Fetch implementation, useful for SSR and tests. Defaults to `globalThis.fetch`. */\n readonly fetch?: typeof globalThis.fetch;\n /** Static headers or an async provider invoked for each logical request. */\n readonly headers?: RequestHeaders;\n /**\n * The signed-in member's credential, sent as `Authorization: Bearer` on\n * PUBLIC scope only.\n *\n * 🔴 Use this rather than putting the credential in `headers`. A configured\n * header applies to every scope, and platform scope rejects a configured\n * Authorization outright — so an app with a signed-in member would be unable\n * to call any platform method the moment a module client gained one. The\n * failure is a TypeError at request time, not a compile error, and it is\n * latent until the first platform-scope method exists.\n */\n readonly memberCredential?: string;\n /** Fetch credentials policy. Defaults to `include`. */\n readonly credentials?: RequestCredentials;\n /** Metadata made available to the header provider on every request. */\n readonly metadata?: RequestMetadata;\n /** Optional access-token lifecycle used only by platform-scope requests. */\n readonly platformAuth?: PlatformAuth;\n /** Maximum bytes parsed from response bodies. Defaults to one mebibyte. */\n readonly maxResponseBytes?: number;\n}\n\nfunction resolveFetch(fetchImplementation: typeof globalThis.fetch | undefined): typeof globalThis.fetch {\n if (fetchImplementation !== undefined) {\n if (typeof fetchImplementation !== \"function\") {\n throw new TypeError(\"fetch must be a function\");\n }\n return fetchImplementation;\n }\n if (typeof globalThis.fetch !== \"function\") {\n throw new TypeError(\"globalThis.fetch is unavailable; provide a fetch implementation\");\n }\n return globalThis.fetch.bind(globalThis);\n}\n\nfunction validatePlatformAuth(platformAuth: PlatformAuth | undefined): void {\n if (platformAuth === undefined) return;\n if (platformAuth === null || typeof platformAuth !== \"object\") {\n throw new TypeError(\"platformAuth must be an object\");\n }\n if (typeof platformAuth.getAccessToken !== \"function\") {\n throw new TypeError(\"platformAuth.getAccessToken must be a function\");\n }\n if (\n platformAuth.refreshAccessToken !== undefined &&\n typeof platformAuth.refreshAccessToken !== \"function\"\n ) {\n throw new TypeError(\"platformAuth.refreshAccessToken must be a function\");\n }\n}\n\n/**\n * Creates an application client from an explicit object of module plugins.\n *\n * The object keys are local aliases and do not affect routing. A module\n * reference may occur only once so two aliases cannot silently address the\n * same installed module with different expectations.\n */\nexport function createAppClient<const TModules extends ModulePluginMap>(\n options: CreateAppClientOptions<TModules>,\n): AppClient<TModules> {\n if (options === null || typeof options !== \"object\") {\n throw new TypeError(\"application client options must be an object\");\n }\n if (options.modules === null || typeof options.modules !== \"object\") {\n throw new TypeError(\"modules must be an object of module plugins\");\n }\n validatePlatformAuth(options.platformAuth);\n\n const entries = Object.entries(options.modules) as [string, ModuleClientPlugin<unknown>][];\n const refs = new Set<string>();\n for (const [alias, plugin] of entries) {\n if (plugin === null || typeof plugin !== \"object\") {\n throw new TypeError(`module ${alias} is not a module client plugin`);\n }\n if (plugin.apiVersion !== MODULE_CLIENT_API_VERSION) {\n throw new TypeError(\n `module ${alias} uses unsupported client API version ${String(plugin.apiVersion)}`,\n );\n }\n assertModuleRef(plugin.moduleRef);\n if (typeof plugin.create !== \"function\") {\n throw new TypeError(`module ${alias} does not provide a create function`);\n }\n if (refs.has(plugin.moduleRef)) {\n throw new TypeError(`duplicate moduleRef registration: ${plugin.moduleRef}`);\n }\n refs.add(plugin.moduleRef);\n }\n\n const shared: SharedTransportConfig = {\n baseUrl: options.baseUrl,\n fetch: resolveFetch(options.fetch),\n credentials: options.credentials ?? \"include\",\n maxResponseBytes: resolveMaxResponseBytes(options.maxResponseBytes),\n ...(options.headers === undefined ? {} : { headers: options.headers }),\n ...(options.memberCredential === undefined ? {} : { memberCredential: options.memberCredential }),\n ...(options.metadata === undefined ? {} : { metadata: options.metadata }),\n ...(options.platformAuth === undefined ? {} : { platformAuth: options.platformAuth }),\n };\n const modules = {} as { [TKey in keyof TModules]: ModuleApi<TModules[TKey]> };\n for (const [alias, plugin] of entries) {\n const api = plugin.create(createScopedTransports(shared, plugin.moduleRef));\n Object.defineProperty(modules, alias, {\n configurable: false,\n enumerable: true,\n value: api,\n writable: false,\n });\n }\n\n return Object.freeze({ modules: Object.freeze(modules) });\n}\n"]}
|
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ModuleScope } from "./plugin.js";
|
|
2
|
+
/** Construction data retained by {@link ModuleClientError}. */
|
|
3
|
+
export interface ModuleClientErrorOptions {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly code?: string;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly details?: unknown;
|
|
8
|
+
readonly body: unknown;
|
|
9
|
+
readonly requestId?: string;
|
|
10
|
+
readonly moduleRef: string;
|
|
11
|
+
readonly scope: ModuleScope;
|
|
12
|
+
readonly path: string;
|
|
13
|
+
}
|
|
14
|
+
/** A non-successful HTTP response returned by a MirrorStack module. */
|
|
15
|
+
export declare class ModuleClientError extends Error {
|
|
16
|
+
readonly status: number;
|
|
17
|
+
readonly code: string | undefined;
|
|
18
|
+
readonly details: unknown;
|
|
19
|
+
readonly body: unknown;
|
|
20
|
+
readonly requestId: string | undefined;
|
|
21
|
+
readonly moduleRef: string;
|
|
22
|
+
readonly scope: ModuleScope;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
constructor(options: ModuleClientErrorOptions);
|
|
25
|
+
}
|
|
26
|
+
/** @internal */
|
|
27
|
+
export declare function errorCodeFromResponse(response: Response, maxResponseBytes?: number): Promise<string | undefined>;
|
|
28
|
+
/** @internal */
|
|
29
|
+
export declare function moduleClientErrorFromResponse(response: Response, context: Pick<ModuleClientErrorOptions, "moduleRef" | "scope" | "path">, maxResponseBytes?: number): Promise<ModuleClientError>;
|
package/dist/error.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { DEFAULT_MAX_RESPONSE_BYTES, readResponseText, } from "./response.js";
|
|
2
|
+
/** A non-successful HTTP response returned by a MirrorStack module. */
|
|
3
|
+
export class ModuleClientError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
details;
|
|
7
|
+
body;
|
|
8
|
+
requestId;
|
|
9
|
+
moduleRef;
|
|
10
|
+
scope;
|
|
11
|
+
path;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super(options.message);
|
|
14
|
+
this.name = "ModuleClientError";
|
|
15
|
+
this.status = options.status;
|
|
16
|
+
this.code = options.code;
|
|
17
|
+
this.details = options.details;
|
|
18
|
+
this.body = options.body;
|
|
19
|
+
this.requestId = options.requestId;
|
|
20
|
+
this.moduleRef = options.moduleRef;
|
|
21
|
+
this.scope = options.scope;
|
|
22
|
+
this.path = options.path;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function stringProperty(value, key) {
|
|
26
|
+
if (value === null || typeof value !== "object")
|
|
27
|
+
return undefined;
|
|
28
|
+
const property = value[key];
|
|
29
|
+
return typeof property === "string" ? property : undefined;
|
|
30
|
+
}
|
|
31
|
+
function parseEnvelope(body) {
|
|
32
|
+
if (body === null || typeof body !== "object")
|
|
33
|
+
return {};
|
|
34
|
+
const record = body;
|
|
35
|
+
const error = record.error;
|
|
36
|
+
const requestId = stringProperty(record, "request_id") ?? stringProperty(record, "requestId");
|
|
37
|
+
if (typeof error === "string") {
|
|
38
|
+
return {
|
|
39
|
+
code: error,
|
|
40
|
+
message: stringProperty(record, "message"),
|
|
41
|
+
details: record.details,
|
|
42
|
+
requestId,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (error !== null && typeof error === "object") {
|
|
46
|
+
return {
|
|
47
|
+
code: stringProperty(error, "code"),
|
|
48
|
+
message: stringProperty(error, "message"),
|
|
49
|
+
details: error.details,
|
|
50
|
+
requestId,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { requestId };
|
|
54
|
+
}
|
|
55
|
+
async function readBody(response, maxResponseBytes) {
|
|
56
|
+
const text = await readResponseText(response, maxResponseBytes);
|
|
57
|
+
if (text === "")
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(text);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** @internal */
|
|
67
|
+
export async function errorCodeFromResponse(response, maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
68
|
+
try {
|
|
69
|
+
return parseEnvelope(await readBody(response, maxResponseBytes)).code;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** @internal */
|
|
76
|
+
export async function moduleClientErrorFromResponse(response, context, maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES) {
|
|
77
|
+
let body = null;
|
|
78
|
+
try {
|
|
79
|
+
body = await readBody(response, maxResponseBytes);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// A broken error body must not hide the useful HTTP context.
|
|
83
|
+
}
|
|
84
|
+
const envelope = parseEnvelope(body);
|
|
85
|
+
const requestId = response.headers.get("x-request-id") ??
|
|
86
|
+
response.headers.get("x-ms-request-id") ??
|
|
87
|
+
envelope.requestId;
|
|
88
|
+
const message = envelope.message ??
|
|
89
|
+
`Module request failed with status ${response.status}${envelope.code === undefined ? "" : ` (${envelope.code})`}`;
|
|
90
|
+
return new ModuleClientError({
|
|
91
|
+
...context,
|
|
92
|
+
status: response.status,
|
|
93
|
+
code: envelope.code,
|
|
94
|
+
message,
|
|
95
|
+
details: envelope.details,
|
|
96
|
+
body,
|
|
97
|
+
...(requestId === null || requestId === undefined ? {} : { requestId }),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ModuleScope } from \"./plugin.js\";\nimport {\n DEFAULT_MAX_RESPONSE_BYTES,\n readResponseText,\n} from \"./response.js\";\n\n/** Construction data retained by {@link ModuleClientError}. */\nexport interface ModuleClientErrorOptions {\n readonly status: number;\n readonly code?: string;\n readonly message: string;\n readonly details?: unknown;\n readonly body: unknown;\n readonly requestId?: string;\n readonly moduleRef: string;\n readonly scope: ModuleScope;\n readonly path: string;\n}\n\n/** A non-successful HTTP response returned by a MirrorStack module. */\nexport class ModuleClientError extends Error {\n readonly status: number;\n readonly code: string | undefined;\n readonly details: unknown;\n readonly body: unknown;\n readonly requestId: string | undefined;\n readonly moduleRef: string;\n readonly scope: ModuleScope;\n readonly path: string;\n\n constructor(options: ModuleClientErrorOptions) {\n super(options.message);\n this.name = \"ModuleClientError\";\n this.status = options.status;\n this.code = options.code;\n this.details = options.details;\n this.body = options.body;\n this.requestId = options.requestId;\n this.moduleRef = options.moduleRef;\n this.scope = options.scope;\n this.path = options.path;\n }\n}\n\ninterface ParsedErrorEnvelope {\n readonly code?: string;\n readonly message?: string;\n readonly details?: unknown;\n readonly requestId?: string;\n}\n\nfunction stringProperty(value: unknown, key: string): string | undefined {\n if (value === null || typeof value !== \"object\") return undefined;\n const property = (value as Record<string, unknown>)[key];\n return typeof property === \"string\" ? property : undefined;\n}\n\nfunction parseEnvelope(body: unknown): ParsedErrorEnvelope {\n if (body === null || typeof body !== \"object\") return {};\n const record = body as Record<string, unknown>;\n const error = record.error;\n const requestId =\n stringProperty(record, \"request_id\") ?? stringProperty(record, \"requestId\");\n\n if (typeof error === \"string\") {\n return {\n code: error,\n message: stringProperty(record, \"message\"),\n details: record.details,\n requestId,\n };\n }\n if (error !== null && typeof error === \"object\") {\n return {\n code: stringProperty(error, \"code\"),\n message: stringProperty(error, \"message\"),\n details: (error as Record<string, unknown>).details,\n requestId,\n };\n }\n return { requestId };\n}\n\nasync function readBody(response: Response, maxResponseBytes: number): Promise<unknown> {\n const text = await readResponseText(response, maxResponseBytes);\n if (text === \"\") return null;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n}\n\n/** @internal */\nexport async function errorCodeFromResponse(\n response: Response,\n maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES,\n): Promise<string | undefined> {\n try {\n return parseEnvelope(await readBody(response, maxResponseBytes)).code;\n } catch {\n return undefined;\n }\n}\n\n/** @internal */\nexport async function moduleClientErrorFromResponse(\n response: Response,\n context: Pick<ModuleClientErrorOptions, \"moduleRef\" | \"scope\" | \"path\">,\n maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES,\n): Promise<ModuleClientError> {\n let body: unknown = null;\n try {\n body = await readBody(response, maxResponseBytes);\n } catch {\n // A broken error body must not hide the useful HTTP context.\n }\n const envelope = parseEnvelope(body);\n const requestId =\n response.headers.get(\"x-request-id\") ??\n response.headers.get(\"x-ms-request-id\") ??\n envelope.requestId;\n const message =\n envelope.message ??\n `Module request failed with status ${response.status}${\n envelope.code === undefined ? \"\" : ` (${envelope.code})`\n }`;\n\n return new ModuleClientError({\n ...context,\n status: response.status,\n code: envelope.code,\n message,\n details: envelope.details,\n body,\n ...(requestId === null || requestId === undefined ? {} : { requestId }),\n });\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MODULE_CLIENT_API_VERSION, defineModuleClient, type ModuleClientContext, type ModuleClientDefinition, type ModuleClientPlugin, type ModuleScope, } from "./plugin.js";
|
|
2
|
+
export { createAppClient, type AppClient, type CreateAppClientOptions, type ModuleApi, type ModulePluginMap, } from "./client.js";
|
|
3
|
+
export { platformBaseUrl, type PlatformBaseUrlOptions } from "./base-url.js";
|
|
4
|
+
export { ModuleClientError, type ModuleClientErrorOptions, } from "./error.js";
|
|
5
|
+
export { DEFAULT_MAX_RESPONSE_BYTES, ModuleResponseTooLargeError, } from "./response.js";
|
|
6
|
+
export type { Awaitable, ModuleRequestContext, ModuleRequestOptions, ModuleResponse, ModuleResponseType, PlatformAuth, QueryParams, QueryScalar, RequestHeaders, RequestMetadata, ScopedFetchOptions, ScopedTransport, } from "./transport.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MODULE_CLIENT_API_VERSION, defineModuleClient, } from "./plugin.js";
|
|
2
|
+
export { createAppClient, } from "./client.js";
|
|
3
|
+
export { platformBaseUrl } from "./base-url.js";
|
|
4
|
+
export { ModuleClientError, } from "./error.js";
|
|
5
|
+
export { DEFAULT_MAX_RESPONSE_BYTES, ModuleResponseTooLargeError, } from "./response.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,kBAAkB,GAKnB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,eAAe,GAKhB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,eAAe,EAA+B,MAAM,eAAe,CAAC;AAE7E,OAAO,EACL,iBAAiB,GAElB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,GAC5B,MAAM,eAAe,CAAC","sourcesContent":["export {\n MODULE_CLIENT_API_VERSION,\n defineModuleClient,\n type ModuleClientContext,\n type ModuleClientDefinition,\n type ModuleClientPlugin,\n type ModuleScope,\n} from \"./plugin.js\";\n\nexport {\n createAppClient,\n type AppClient,\n type CreateAppClientOptions,\n type ModuleApi,\n type ModulePluginMap,\n} from \"./client.js\";\n\nexport { platformBaseUrl, type PlatformBaseUrlOptions } from \"./base-url.js\";\n\nexport {\n ModuleClientError,\n type ModuleClientErrorOptions,\n} from \"./error.js\";\n\nexport {\n DEFAULT_MAX_RESPONSE_BYTES,\n ModuleResponseTooLargeError,\n} from \"./response.js\";\n\nexport type {\n Awaitable,\n ModuleRequestContext,\n ModuleRequestOptions,\n ModuleResponse,\n ModuleResponseType,\n PlatformAuth,\n QueryParams,\n QueryScalar,\n RequestHeaders,\n RequestMetadata,\n ScopedFetchOptions,\n ScopedTransport,\n} from \"./transport.js\";\n"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type MemberSessionsApi } from "../server/member-sessions.js";
|
|
2
|
+
/**
|
|
3
|
+
* Ready-made sign-in routes for a custom web app on the Next.js App Router.
|
|
4
|
+
*
|
|
5
|
+
* A custom app runs on its own origin, and the auth provider's own session
|
|
6
|
+
* cookie lives on the platform host, so the app has to (1) issue a one-time
|
|
7
|
+
* state and send the browser to the provider, (2) take the one-time code the
|
|
8
|
+
* provider appends to the redirect and exchange it for a platform member
|
|
9
|
+
* session, (3) keep that credential in an HttpOnly cookie on its origin, and
|
|
10
|
+
* (4) revoke it on sign-out. Every custom app needs exactly these four steps,
|
|
11
|
+
* so they live here once, and an app's route file is one export line:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* // src/lib/auth.ts
|
|
15
|
+
* export const auth = createAuthRoutes({ apiUrl, appSlug, provider: client.modules.userCore });
|
|
16
|
+
* // src/app/api/auth/start/route.ts
|
|
17
|
+
* export const { GET } = auth.start;
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
/** The part of an auth-provider plugin this adapter needs: how to start a sign-in. */
|
|
21
|
+
export interface AuthProviderStart {
|
|
22
|
+
startUrl(provider: string, options: {
|
|
23
|
+
redirect: string;
|
|
24
|
+
handoffState: string;
|
|
25
|
+
}): string;
|
|
26
|
+
}
|
|
27
|
+
/** Inputs for {@link createAuthRoutes}. */
|
|
28
|
+
export interface AuthRoutesOptions {
|
|
29
|
+
/** Absolute HTTP(S) platform API URL, typically `MIRRORSTACK_API_URL`. */
|
|
30
|
+
readonly apiUrl: string;
|
|
31
|
+
/** The custom application's slug, typically `MIRRORSTACK_APP_SLUG`. */
|
|
32
|
+
readonly appSlug: string;
|
|
33
|
+
/** The auth-provider plugin that builds the sign-in URL, e.g. `client.modules.userCore`. */
|
|
34
|
+
readonly provider: AuthProviderStart;
|
|
35
|
+
/**
|
|
36
|
+
* The query parameter the provider appends to the redirect with the one-time
|
|
37
|
+
* code. User Core sends `ms_handoff`; take the name from the provider's
|
|
38
|
+
* client when it exports one.
|
|
39
|
+
*
|
|
40
|
+
* @defaultValue `"ms_handoff"`
|
|
41
|
+
*/
|
|
42
|
+
readonly handoffParam?: string;
|
|
43
|
+
/** App paths. Defaults: callback `/api/auth/callback`, login `/login`, home `/`. */
|
|
44
|
+
readonly paths?: {
|
|
45
|
+
readonly callback?: string;
|
|
46
|
+
readonly login?: string;
|
|
47
|
+
readonly home?: string;
|
|
48
|
+
};
|
|
49
|
+
/** Cookie names and lifetimes. */
|
|
50
|
+
readonly cookies?: {
|
|
51
|
+
/** @defaultValue `"ms_member_session"` */
|
|
52
|
+
readonly session?: string;
|
|
53
|
+
/** @defaultValue `"ms_handoff_state"` */
|
|
54
|
+
readonly state?: string;
|
|
55
|
+
/** @defaultValue 8 hours */
|
|
56
|
+
readonly sessionMaxAgeSeconds?: number;
|
|
57
|
+
/** @defaultValue 10 minutes */
|
|
58
|
+
readonly stateMaxAgeSeconds?: number;
|
|
59
|
+
};
|
|
60
|
+
/** Fetch implementation for the platform calls. Defaults to `globalThis.fetch`. */
|
|
61
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
62
|
+
}
|
|
63
|
+
/** What {@link createAuthRoutes} returns. */
|
|
64
|
+
export interface AuthRoutes {
|
|
65
|
+
/** `GET /api/auth/start?provider=<slug>` — issue state, redirect to the provider. */
|
|
66
|
+
readonly start: {
|
|
67
|
+
GET(request: Request): Promise<Response>;
|
|
68
|
+
};
|
|
69
|
+
/** `GET /api/auth/callback?<handoffParam>=<code>` — exchange the code, set the session cookie. */
|
|
70
|
+
readonly callback: {
|
|
71
|
+
GET(request: Request): Promise<Response>;
|
|
72
|
+
};
|
|
73
|
+
/** `POST /api/auth/logout` — revoke on the platform, then clear the cookie. */
|
|
74
|
+
readonly logout: {
|
|
75
|
+
POST(request: Request): Promise<Response>;
|
|
76
|
+
};
|
|
77
|
+
/** The member credential for the current request, or null when signed out. Server-side only. */
|
|
78
|
+
readMemberCredential(): Promise<string | null>;
|
|
79
|
+
/** The member-sessions API these routes use, for callers that need it directly. */
|
|
80
|
+
readonly sessions: MemberSessionsApi;
|
|
81
|
+
}
|
|
82
|
+
export declare function createAuthRoutes(options: AuthRoutesOptions): AuthRoutes;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { cookies } from "next/headers.js";
|
|
2
|
+
import { memberSessions, } from "../server/member-sessions.js";
|
|
3
|
+
const DEFAULT_SESSION_COOKIE = "ms_member_session";
|
|
4
|
+
const DEFAULT_STATE_COOKIE = "ms_handoff_state";
|
|
5
|
+
const DEFAULT_SESSION_MAX_AGE = 8 * 60 * 60;
|
|
6
|
+
const DEFAULT_STATE_MAX_AGE = 10 * 60;
|
|
7
|
+
const DEFAULT_HANDOFF_PARAM = "ms_handoff";
|
|
8
|
+
const COOKIE_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
9
|
+
const PROVIDER_SLUG_PATTERN = /^[a-z][a-z0-9-]{0,15}$/;
|
|
10
|
+
function assertPath(value, label) {
|
|
11
|
+
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//")) {
|
|
12
|
+
throw new TypeError(`${label} must be an absolute path on this app`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function assertCookieName(value, label) {
|
|
17
|
+
if (typeof value !== "string" || !COOKIE_NAME_PATTERN.test(value)) {
|
|
18
|
+
throw new TypeError(`${label} must be a cookie name matching [A-Za-z0-9_-]{1,64}`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function redirect(request, path, status = 302) {
|
|
23
|
+
return Response.redirect(new URL(path, request.url).toString(), status);
|
|
24
|
+
}
|
|
25
|
+
function failure(request, loginPath, reason) {
|
|
26
|
+
return redirect(request, `${loginPath}?error=${encodeURIComponent(reason)}`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Creates the three sign-in route handlers and the credential reader for one
|
|
30
|
+
* custom application. Inputs are validated up front, like `platformBaseUrl`.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Report whether the BROWSER reached this app over HTTPS.
|
|
34
|
+
*
|
|
35
|
+
* 🔴 Not `new URL(request.url).protocol` alone. Behind a TLS-terminating proxy
|
|
36
|
+
* — which is every production deployment of this framework — the request URL
|
|
37
|
+
* carries the scheme of the INTERNAL hop, so it reads "http:" while the browser
|
|
38
|
+
* is on HTTPS. Deriving `Secure` from it therefore ships the SESSION COOKIE
|
|
39
|
+
* without the Secure attribute over a connection the user believes is
|
|
40
|
+
* encrypted, and a cookie without Secure is sent on any later plaintext request
|
|
41
|
+
* to the same host.
|
|
42
|
+
*
|
|
43
|
+
* `x-forwarded-proto` is what the proxy sets to say what the browser used, and
|
|
44
|
+
* it is only ever consulted to ADD Secure, never to remove it: a forged header
|
|
45
|
+
* cannot weaken the cookie, only harden it. A comma list ("https,http") keeps
|
|
46
|
+
* the first hop, which is the browser's.
|
|
47
|
+
*
|
|
48
|
+
* @param request - The incoming route request.
|
|
49
|
+
* @returns True when the cookie must carry `Secure`.
|
|
50
|
+
*/
|
|
51
|
+
function isSecureRequest(request) {
|
|
52
|
+
const forwarded = request.headers.get("x-forwarded-proto");
|
|
53
|
+
if (forwarded && forwarded.split(",")[0].trim().toLowerCase() === "https") {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
// Plain HTTP local development sets neither, and a `Secure` cookie cannot be
|
|
57
|
+
// stored there — so the fallback stays the request's own scheme.
|
|
58
|
+
return new URL(request.url).protocol === "https:";
|
|
59
|
+
}
|
|
60
|
+
export function createAuthRoutes(options) {
|
|
61
|
+
if (options === null || typeof options !== "object") {
|
|
62
|
+
throw new TypeError("createAuthRoutes options must be an object");
|
|
63
|
+
}
|
|
64
|
+
const provider = options.provider;
|
|
65
|
+
if (provider === null || typeof provider !== "object" || typeof provider.startUrl !== "function") {
|
|
66
|
+
throw new TypeError("provider must expose startUrl(provider, { redirect, handoffState })");
|
|
67
|
+
}
|
|
68
|
+
const sessions = memberSessions({
|
|
69
|
+
apiUrl: options.apiUrl,
|
|
70
|
+
appSlug: options.appSlug,
|
|
71
|
+
fetch: options.fetch,
|
|
72
|
+
});
|
|
73
|
+
const handoffParam = options.handoffParam ?? DEFAULT_HANDOFF_PARAM;
|
|
74
|
+
if (typeof handoffParam !== "string" || handoffParam.length === 0) {
|
|
75
|
+
throw new TypeError("handoffParam must be a non-empty query parameter name");
|
|
76
|
+
}
|
|
77
|
+
const callbackPath = assertPath(options.paths?.callback ?? "/api/auth/callback", "paths.callback");
|
|
78
|
+
const loginPath = assertPath(options.paths?.login ?? "/login", "paths.login");
|
|
79
|
+
const homePath = assertPath(options.paths?.home ?? "/", "paths.home");
|
|
80
|
+
const sessionCookie = assertCookieName(options.cookies?.session ?? DEFAULT_SESSION_COOKIE, "cookies.session");
|
|
81
|
+
const stateCookie = assertCookieName(options.cookies?.state ?? DEFAULT_STATE_COOKIE, "cookies.state");
|
|
82
|
+
const sessionMaxAge = options.cookies?.sessionMaxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE;
|
|
83
|
+
const stateMaxAge = options.cookies?.stateMaxAgeSeconds ?? DEFAULT_STATE_MAX_AGE;
|
|
84
|
+
// Never `SameSite=None`: the return leg from the provider is a top-level GET
|
|
85
|
+
// navigation, which Lax still carries.
|
|
86
|
+
const cookieOptions = (request, maxAge) => ({
|
|
87
|
+
httpOnly: true,
|
|
88
|
+
sameSite: "lax",
|
|
89
|
+
secure: isSecureRequest(request),
|
|
90
|
+
path: "/",
|
|
91
|
+
maxAge,
|
|
92
|
+
});
|
|
93
|
+
async function readMemberCredential() {
|
|
94
|
+
const store = await cookies();
|
|
95
|
+
return store.get(sessionCookie)?.value ?? null;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
sessions,
|
|
99
|
+
readMemberCredential,
|
|
100
|
+
start: {
|
|
101
|
+
async GET(request) {
|
|
102
|
+
const slug = new URL(request.url).searchParams.get("provider") ?? "";
|
|
103
|
+
if (!PROVIDER_SLUG_PATTERN.test(slug)) {
|
|
104
|
+
return Response.json({ error: "provider is required" }, { status: 400 });
|
|
105
|
+
}
|
|
106
|
+
// The state is issued and stored in the SAME request that hands out
|
|
107
|
+
// the URL carrying it, so the cookie and the URL cannot drift apart.
|
|
108
|
+
const state = sessions.newState();
|
|
109
|
+
const store = await cookies();
|
|
110
|
+
store.set(stateCookie, state, cookieOptions(request, stateMaxAge));
|
|
111
|
+
// The callback origin is the origin of THIS request, never a
|
|
112
|
+
// configured value: the same build serves localhost and production,
|
|
113
|
+
// and the provider's redirect allowlist is the guard against an
|
|
114
|
+
// attacker-chosen origin.
|
|
115
|
+
const redirectTo = new URL(callbackPath, request.url).toString();
|
|
116
|
+
return redirect(request, provider.startUrl(slug, { redirect: redirectTo, handoffState: state }));
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
callback: {
|
|
120
|
+
async GET(request) {
|
|
121
|
+
const code = new URL(request.url).searchParams.get(handoffParam);
|
|
122
|
+
// Taken (and cleared) BEFORE anything is redeemed, so a replayed URL
|
|
123
|
+
// finds nothing to match against. The provider echoes no state; the
|
|
124
|
+
// binding is this cookie posted with the code, which the platform
|
|
125
|
+
// refuses unless the pair matches the handoff it recorded.
|
|
126
|
+
const store = await cookies();
|
|
127
|
+
const issuedState = store.get(stateCookie)?.value ?? null;
|
|
128
|
+
store.delete(stateCookie);
|
|
129
|
+
if (!code)
|
|
130
|
+
return failure(request, loginPath, "missing_code");
|
|
131
|
+
if (!issuedState)
|
|
132
|
+
return failure(request, loginPath, "expired");
|
|
133
|
+
let credential;
|
|
134
|
+
try {
|
|
135
|
+
credential = (await sessions.exchange(code, issuedState)).credential;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return failure(request, loginPath, "exchange_failed");
|
|
139
|
+
}
|
|
140
|
+
store.set(sessionCookie, credential, cookieOptions(request, sessionMaxAge));
|
|
141
|
+
return redirect(request, homePath);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
logout: {
|
|
145
|
+
async POST(request) {
|
|
146
|
+
const store = await cookies();
|
|
147
|
+
const credential = store.get(sessionCookie)?.value ?? null;
|
|
148
|
+
if (credential) {
|
|
149
|
+
// Forgetting the cookie is not a sign-out: the credential stays
|
|
150
|
+
// valid on the platform until it expires. Clear only once the
|
|
151
|
+
// platform no longer honours it; otherwise the member stays signed
|
|
152
|
+
// in and is told, rather than shown a sign-out that did not happen.
|
|
153
|
+
const outcome = await sessions.revoke(credential);
|
|
154
|
+
if (outcome === "unavailable") {
|
|
155
|
+
return redirect(request, `${homePath}?error=logout_unavailable`, 303);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
store.delete(sessionCookie);
|
|
159
|
+
return redirect(request, loginPath, 303);
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=auth-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-routes.js","sourceRoot":"","sources":["../../src/next/auth-routes.ts"],"names":[],"mappings":"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","sourcesContent":["import { cookies } from \"next/headers.js\";\nimport {\n memberSessions,\n type MemberSessionsApi,\n} from \"../server/member-sessions.js\";\n\n/**\n * Ready-made sign-in routes for a custom web app on the Next.js App Router.\n *\n * A custom app runs on its own origin, and the auth provider's own session\n * cookie lives on the platform host, so the app has to (1) issue a one-time\n * state and send the browser to the provider, (2) take the one-time code the\n * provider appends to the redirect and exchange it for a platform member\n * session, (3) keep that credential in an HttpOnly cookie on its origin, and\n * (4) revoke it on sign-out. Every custom app needs exactly these four steps,\n * so they live here once, and an app's route file is one export line:\n *\n * ```ts\n * // src/lib/auth.ts\n * export const auth = createAuthRoutes({ apiUrl, appSlug, provider: client.modules.userCore });\n * // src/app/api/auth/start/route.ts\n * export const { GET } = auth.start;\n * ```\n */\n\n/** The part of an auth-provider plugin this adapter needs: how to start a sign-in. */\nexport interface AuthProviderStart {\n startUrl(provider: string, options: { redirect: string; handoffState: string }): string;\n}\n\n/** Inputs for {@link createAuthRoutes}. */\nexport interface AuthRoutesOptions {\n /** Absolute HTTP(S) platform API URL, typically `MIRRORSTACK_API_URL`. */\n readonly apiUrl: string;\n /** The custom application's slug, typically `MIRRORSTACK_APP_SLUG`. */\n readonly appSlug: string;\n /** The auth-provider plugin that builds the sign-in URL, e.g. `client.modules.userCore`. */\n readonly provider: AuthProviderStart;\n /**\n * The query parameter the provider appends to the redirect with the one-time\n * code. User Core sends `ms_handoff`; take the name from the provider's\n * client when it exports one.\n *\n * @defaultValue `\"ms_handoff\"`\n */\n readonly handoffParam?: string;\n /** App paths. Defaults: callback `/api/auth/callback`, login `/login`, home `/`. */\n readonly paths?: {\n readonly callback?: string;\n readonly login?: string;\n readonly home?: string;\n };\n /** Cookie names and lifetimes. */\n readonly cookies?: {\n /** @defaultValue `\"ms_member_session\"` */\n readonly session?: string;\n /** @defaultValue `\"ms_handoff_state\"` */\n readonly state?: string;\n /** @defaultValue 8 hours */\n readonly sessionMaxAgeSeconds?: number;\n /** @defaultValue 10 minutes */\n readonly stateMaxAgeSeconds?: number;\n };\n /** Fetch implementation for the platform calls. Defaults to `globalThis.fetch`. */\n readonly fetch?: typeof globalThis.fetch;\n}\n\n/** What {@link createAuthRoutes} returns. */\nexport interface AuthRoutes {\n /** `GET /api/auth/start?provider=<slug>` — issue state, redirect to the provider. */\n readonly start: { GET(request: Request): Promise<Response> };\n /** `GET /api/auth/callback?<handoffParam>=<code>` — exchange the code, set the session cookie. */\n readonly callback: { GET(request: Request): Promise<Response> };\n /** `POST /api/auth/logout` — revoke on the platform, then clear the cookie. */\n readonly logout: { POST(request: Request): Promise<Response> };\n /** The member credential for the current request, or null when signed out. Server-side only. */\n readMemberCredential(): Promise<string | null>;\n /** The member-sessions API these routes use, for callers that need it directly. */\n readonly sessions: MemberSessionsApi;\n}\n\nconst DEFAULT_SESSION_COOKIE = \"ms_member_session\";\nconst DEFAULT_STATE_COOKIE = \"ms_handoff_state\";\nconst DEFAULT_SESSION_MAX_AGE = 8 * 60 * 60;\nconst DEFAULT_STATE_MAX_AGE = 10 * 60;\nconst DEFAULT_HANDOFF_PARAM = \"ms_handoff\";\nconst COOKIE_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;\nconst PROVIDER_SLUG_PATTERN = /^[a-z][a-z0-9-]{0,15}$/;\n\nfunction assertPath(value: string, label: string): string {\n if (typeof value !== \"string\" || !value.startsWith(\"/\") || value.startsWith(\"//\")) {\n throw new TypeError(`${label} must be an absolute path on this app`);\n }\n return value;\n}\n\nfunction assertCookieName(value: string, label: string): string {\n if (typeof value !== \"string\" || !COOKIE_NAME_PATTERN.test(value)) {\n throw new TypeError(`${label} must be a cookie name matching [A-Za-z0-9_-]{1,64}`);\n }\n return value;\n}\n\nfunction redirect(request: Request, path: string, status: 302 | 303 = 302): Response {\n return Response.redirect(new URL(path, request.url).toString(), status);\n}\n\nfunction failure(request: Request, loginPath: string, reason: string): Response {\n return redirect(request, `${loginPath}?error=${encodeURIComponent(reason)}`);\n}\n\n/**\n * Creates the three sign-in route handlers and the credential reader for one\n * custom application. Inputs are validated up front, like `platformBaseUrl`.\n */\n/**\n * Report whether the BROWSER reached this app over HTTPS.\n *\n * 🔴 Not `new URL(request.url).protocol` alone. Behind a TLS-terminating proxy\n * — which is every production deployment of this framework — the request URL\n * carries the scheme of the INTERNAL hop, so it reads \"http:\" while the browser\n * is on HTTPS. Deriving `Secure` from it therefore ships the SESSION COOKIE\n * without the Secure attribute over a connection the user believes is\n * encrypted, and a cookie without Secure is sent on any later plaintext request\n * to the same host.\n *\n * `x-forwarded-proto` is what the proxy sets to say what the browser used, and\n * it is only ever consulted to ADD Secure, never to remove it: a forged header\n * cannot weaken the cookie, only harden it. A comma list (\"https,http\") keeps\n * the first hop, which is the browser's.\n *\n * @param request - The incoming route request.\n * @returns True when the cookie must carry `Secure`.\n */\nfunction isSecureRequest(request: Request): boolean {\n const forwarded = request.headers.get(\"x-forwarded-proto\");\n if (forwarded && forwarded.split(\",\")[0]!.trim().toLowerCase() === \"https\") {\n return true;\n }\n // Plain HTTP local development sets neither, and a `Secure` cookie cannot be\n // stored there — so the fallback stays the request's own scheme.\n return new URL(request.url).protocol === \"https:\";\n}\n\nexport function createAuthRoutes(options: AuthRoutesOptions): AuthRoutes {\n if (options === null || typeof options !== \"object\") {\n throw new TypeError(\"createAuthRoutes options must be an object\");\n }\n const provider = options.provider;\n if (provider === null || typeof provider !== \"object\" || typeof provider.startUrl !== \"function\") {\n throw new TypeError(\"provider must expose startUrl(provider, { redirect, handoffState })\");\n }\n const sessions = memberSessions({\n apiUrl: options.apiUrl,\n appSlug: options.appSlug,\n fetch: options.fetch,\n });\n const handoffParam = options.handoffParam ?? DEFAULT_HANDOFF_PARAM;\n if (typeof handoffParam !== \"string\" || handoffParam.length === 0) {\n throw new TypeError(\"handoffParam must be a non-empty query parameter name\");\n }\n const callbackPath = assertPath(options.paths?.callback ?? \"/api/auth/callback\", \"paths.callback\");\n const loginPath = assertPath(options.paths?.login ?? \"/login\", \"paths.login\");\n const homePath = assertPath(options.paths?.home ?? \"/\", \"paths.home\");\n const sessionCookie = assertCookieName(options.cookies?.session ?? DEFAULT_SESSION_COOKIE, \"cookies.session\");\n const stateCookie = assertCookieName(options.cookies?.state ?? DEFAULT_STATE_COOKIE, \"cookies.state\");\n const sessionMaxAge = options.cookies?.sessionMaxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE;\n const stateMaxAge = options.cookies?.stateMaxAgeSeconds ?? DEFAULT_STATE_MAX_AGE;\n\n // Never `SameSite=None`: the return leg from the provider is a top-level GET\n // navigation, which Lax still carries.\n const cookieOptions = (request: Request, maxAge: number) => ({\n httpOnly: true,\n sameSite: \"lax\" as const,\n secure: isSecureRequest(request),\n path: \"/\",\n maxAge,\n });\n\n async function readMemberCredential(): Promise<string | null> {\n const store = await cookies();\n return store.get(sessionCookie)?.value ?? null;\n }\n\n return {\n sessions,\n readMemberCredential,\n start: {\n async GET(request) {\n const slug = new URL(request.url).searchParams.get(\"provider\") ?? \"\";\n if (!PROVIDER_SLUG_PATTERN.test(slug)) {\n return Response.json({ error: \"provider is required\" }, { status: 400 });\n }\n // The state is issued and stored in the SAME request that hands out\n // the URL carrying it, so the cookie and the URL cannot drift apart.\n const state = sessions.newState();\n const store = await cookies();\n store.set(stateCookie, state, cookieOptions(request, stateMaxAge));\n // The callback origin is the origin of THIS request, never a\n // configured value: the same build serves localhost and production,\n // and the provider's redirect allowlist is the guard against an\n // attacker-chosen origin.\n const redirectTo = new URL(callbackPath, request.url).toString();\n return redirect(request, provider.startUrl(slug, { redirect: redirectTo, handoffState: state }));\n },\n },\n callback: {\n async GET(request) {\n const code = new URL(request.url).searchParams.get(handoffParam);\n // Taken (and cleared) BEFORE anything is redeemed, so a replayed URL\n // finds nothing to match against. The provider echoes no state; the\n // binding is this cookie posted with the code, which the platform\n // refuses unless the pair matches the handoff it recorded.\n const store = await cookies();\n const issuedState = store.get(stateCookie)?.value ?? null;\n store.delete(stateCookie);\n if (!code) return failure(request, loginPath, \"missing_code\");\n if (!issuedState) return failure(request, loginPath, \"expired\");\n let credential: string;\n try {\n credential = (await sessions.exchange(code, issuedState)).credential;\n } catch {\n return failure(request, loginPath, \"exchange_failed\");\n }\n store.set(sessionCookie, credential, cookieOptions(request, sessionMaxAge));\n return redirect(request, homePath);\n },\n },\n logout: {\n async POST(request) {\n const store = await cookies();\n const credential = store.get(sessionCookie)?.value ?? null;\n if (credential) {\n // Forgetting the cookie is not a sign-out: the credential stays\n // valid on the platform until it expires. Clear only once the\n // platform no longer honours it; otherwise the member stays signed\n // in and is told, rather than shown a sign-out that did not happen.\n const outcome = await sessions.revoke(credential);\n if (outcome === \"unavailable\") {\n return redirect(request, `${homePath}?error=logout_unavailable`, 303);\n }\n }\n store.delete(sessionCookie);\n return redirect(request, loginPath, 303);\n },\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router adapter for a custom web app: ready-made start /
|
|
3
|
+
* callback / logout route handlers over HttpOnly cookies on top of the
|
|
4
|
+
* platform member-session control plane in `./server`, plus the catch-all
|
|
5
|
+
* module proxy the browser client talks through.
|
|
6
|
+
*
|
|
7
|
+
* Requires `next` (optional peer dependency).
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export { createAuthRoutes, type AuthProviderStart, type AuthRoutes, type AuthRoutesOptions, } from "./auth-routes.js";
|
|
12
|
+
export { createModuleProxyRoutes, type ModuleProxyRoutes, type ModuleProxyRoutesOptions, type RouteContext, } from "./module-proxy-routes.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router adapter for a custom web app: ready-made start /
|
|
3
|
+
* callback / logout route handlers over HttpOnly cookies on top of the
|
|
4
|
+
* platform member-session control plane in `./server`, plus the catch-all
|
|
5
|
+
* module proxy the browser client talks through.
|
|
6
|
+
*
|
|
7
|
+
* Requires `next` (optional peer dependency).
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export { createAuthRoutes, } from "./auth-routes.js";
|
|
12
|
+
export { createModuleProxyRoutes, } from "./module-proxy-routes.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,gBAAgB,GAIjB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,uBAAuB,GAIxB,MAAM,0BAA0B,CAAC","sourcesContent":["/**\n * Next.js App Router adapter for a custom web app: ready-made start /\n * callback / logout route handlers over HttpOnly cookies on top of the\n * platform member-session control plane in `./server`, plus the catch-all\n * module proxy the browser client talks through.\n *\n * Requires `next` (optional peer dependency).\n *\n * @packageDocumentation\n */\n\nexport {\n createAuthRoutes,\n type AuthProviderStart,\n type AuthRoutes,\n type AuthRoutesOptions,\n} from \"./auth-routes.js\";\n\nexport {\n createModuleProxyRoutes,\n type ModuleProxyRoutes,\n type ModuleProxyRoutesOptions,\n type RouteContext,\n} from \"./module-proxy-routes.js\";\n"]}
|