@geekapps/auth-nextjs 0.22.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,27 +14,117 @@ __export(typegen_exports, {
14
14
  DEFAULT_ISSUER: () => DEFAULT_ISSUER,
15
15
  DEFAULT_OUT_DIR: () => DEFAULT_OUT_DIR,
16
16
  fetchCatalog: () => fetchCatalog,
17
- generateConnectionTypes: () => generateConnectionTypes
17
+ fetchManifest: () => fetchManifest,
18
+ generateConnectionTypes: () => generateConnectionTypes,
19
+ generateManifestTypes: () => generateManifestTypes
18
20
  });
19
21
  import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "fs";
20
22
  import { join, resolve } from "path";
21
- async function fetchCatalog({ issuer, clientId, clientSecret }) {
22
- const response = await fetch(`${issuer.replace(/\/$/, "")}/connections/catalog`, {
23
- headers: { authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}` },
23
+ async function fetchCatalog(credentials) {
24
+ const issuer = credentials.issuer.replace(/\/$/, "");
25
+ const asUser = "accessToken" in credentials;
26
+ const response = await fetch(`${issuer}${asUser ? "/me/connections/catalog" : "/connections/catalog"}`, {
27
+ headers: {
28
+ authorization: asUser ? `Bearer ${credentials.accessToken}` : `Basic ${Buffer.from(`${credentials.clientId}:${credentials.clientSecret}`).toString("base64")}`
29
+ },
24
30
  cache: "no-store"
25
31
  });
26
32
  if (!response.ok) {
27
- throw new Error(`cat\xE1logo indispon\xEDvel (${response.status}) \u2014 confira GEEKAPPS_AUTH_CLIENT_ID e GEEKAPPS_AUTH_CLIENT_SECRET`);
33
+ throw new Error(
34
+ asUser ? `cat\xE1logo indispon\xEDvel (${response.status}) \u2014 rode \`geekapps login\` de novo` : `cat\xE1logo indispon\xEDvel (${response.status}) \u2014 confira GEEKAPPS_AUTH_CLIENT_ID e GEEKAPPS_AUTH_CLIENT_SECRET`
35
+ );
28
36
  }
29
37
  return await response.json();
30
38
  }
39
+ async function fetchManifest(manifestUrl) {
40
+ const response = await fetch(manifestUrl, { headers: { accept: "application/json" }, cache: "no-store" });
41
+ if (!response.ok) throw new Error(`manifesto indispon\xEDvel (${response.status})`);
42
+ const manifest = await response.json();
43
+ if (manifest?.version !== 1 || !manifest.scopes || !Array.isArray(manifest.routes)) {
44
+ throw new Error("manifesto em formato inv\xE1lido");
45
+ }
46
+ return manifest;
47
+ }
48
+ function pascal(name) {
49
+ return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join("");
50
+ }
51
+ function properties(schema) {
52
+ return schema?.properties ?? {};
53
+ }
54
+ function required(schema) {
55
+ return Array.isArray(schema?.required) ? schema.required : [];
56
+ }
57
+ function mergeInputSchema(route) {
58
+ const parts = [route.params, route.query, route.body];
59
+ const pathParams = [...route.path.matchAll(/:([A-Za-z0-9_]+)/g)].map((m) => m[1]);
60
+ return {
61
+ type: "object",
62
+ properties: Object.assign({}, ...parts.map(properties)),
63
+ required: [.../* @__PURE__ */ new Set([...pathParams, ...parts.flatMap(required)])],
64
+ additionalProperties: false
65
+ };
66
+ }
67
+ async function generateManifestTypes(app, manifest) {
68
+ const { compile } = await import("json-schema-to-typescript");
69
+ const options = { bannerComment: "", additionalProperties: false, format: false, declareExternallyReferenced: true };
70
+ const blocks = [];
71
+ const routeEntries = [];
72
+ for (const route of manifest.routes) {
73
+ const base = pascal(route.route);
74
+ const input = `${base}Input`;
75
+ const output = `${base}Output`;
76
+ blocks.push(await compile(mergeInputSchema(route), input, options));
77
+ blocks.push(
78
+ route.response ? await compile({ ...route.response, title: void 0 }, output, options) : `export type ${output} = unknown;
79
+ `
80
+ );
81
+ routeEntries.push(
82
+ ` ${JSON.stringify(route.route)}: {
83
+ scope: ${JSON.stringify(route.scope)};
84
+ method: ${JSON.stringify(route.method)};
85
+ path: ${JSON.stringify(route.path)};
86
+ input: ${input};
87
+ output: ${output};
88
+ };`
89
+ );
90
+ }
91
+ const scopeIds = Object.keys(manifest.scopes);
92
+ const scopeDocs = scopeIds.map((id) => ` ${JSON.stringify(id)}: ${JSON.stringify(manifest.scopes[id].description)};`).join("\n");
93
+ return `${HEADER}/** Escopos que esta app aceita em conex\xF5es. */
94
+ export type Scope = ${scopeIds.length ? scopeIds.map((id) => JSON.stringify(id)).join(" | ") : "never"};
95
+
96
+ /** Descri\xE7\xE3o de cada escopo, como aparece no consent. */
97
+ export interface ScopeDescriptions {
98
+ ${scopeDocs}
99
+ }
100
+
101
+ /** Rotas liberadas por escopo, pelo nome usado no cliente (\`post.messages.send(...)\`). */
102
+ export interface Routes {
103
+ ${routeEntries.join("\n")}
104
+ }
105
+
106
+ export interface App {
107
+ slug: ${JSON.stringify(app.slug)};
108
+ targetAppId: ${JSON.stringify(app.targetAppId)};
109
+ scopes: Scope;
110
+ routes: Routes;
111
+ }
112
+
113
+ ${blocks.join("\n")}`;
114
+ }
31
115
  async function generateOpenApiTypes(openApiUrl) {
32
116
  const { default: openapiTS, astToString } = await import("openapi-typescript");
33
117
  return HEADER + astToString(await openapiTS(new URL(openApiUrl)));
34
118
  }
35
119
  function renderIndex(apps, augment) {
36
- const imports = apps.map((app, i) => `import type { paths as paths${i} } from "./${app.slug}";`).join("\n");
37
- const members = apps.map((app, i) => ` ${JSON.stringify(app.slug)}: paths${i};`).join("\n");
120
+ const imports = apps.flatMap(({ app, manifest, openApi }, i) => [
121
+ manifest ? `import type { App as App${i} } from "./${app.slug}";` : null,
122
+ openApi ? `import type { paths as paths${i} } from "./${app.slug}.openapi";` : null
123
+ ]).filter(Boolean).join("\n");
124
+ const members = apps.map(({ app, manifest, openApi }, i) => {
125
+ const fields = [manifest ? `app: App${i};` : null, openApi ? `paths: paths${i};` : null].filter(Boolean).join(" ");
126
+ return ` ${JSON.stringify(app.slug)}: { ${fields} };`;
127
+ }).join("\n");
38
128
  return `${HEADER}${imports}
39
129
 
40
130
  declare module ${JSON.stringify(augment)} {
@@ -52,22 +142,38 @@ async function generateConnectionTypes(params) {
52
142
  const files = /* @__PURE__ */ new Map();
53
143
  const result = { apps: [], failed: [], stale: [] };
54
144
  const generated = [];
145
+ const keepExisting = (file) => {
146
+ const existing = join(outDir, file);
147
+ if (!existsSync(existing)) return false;
148
+ files.set(file, readFileSync(existing, "utf-8"));
149
+ return true;
150
+ };
55
151
  for (const app of catalog) {
56
- const file = `${app.slug}.d.ts`;
57
- try {
58
- files.set(file, await generateOpenApiTypes(app.openApiUrl));
59
- generated.push(app);
60
- } catch (error) {
61
- result.failed.push({ name: app.name, reason: error.message });
62
- const existing = join(outDir, file);
63
- if (existsSync(existing)) {
64
- files.set(file, readFileSync(existing, "utf-8"));
65
- generated.push(app);
152
+ const entry = { app, manifest: false, openApi: false };
153
+ if (app.manifestUrl) {
154
+ const file = `${app.slug}.d.ts`;
155
+ try {
156
+ files.set(file, await generateManifestTypes(app, await fetchManifest(app.manifestUrl)));
157
+ entry.manifest = true;
158
+ } catch (error) {
159
+ result.failed.push({ name: app.name, reason: `manifesto: ${error.message}` });
160
+ entry.manifest = keepExisting(file);
66
161
  }
67
162
  }
163
+ if (app.openApiUrl) {
164
+ const file = `${app.slug}.openapi.d.ts`;
165
+ try {
166
+ files.set(file, await generateOpenApiTypes(app.openApiUrl));
167
+ entry.openApi = true;
168
+ } catch (error) {
169
+ result.failed.push({ name: app.name, reason: `openapi: ${error.message}` });
170
+ entry.openApi = keepExisting(file);
171
+ }
172
+ }
173
+ if (entry.manifest || entry.openApi) generated.push(entry);
68
174
  }
69
- result.apps = generated.map((app) => app.name);
70
- files.set("index.d.ts", renderIndex(generated, params.augment ?? "@geekapps/auth-nextjs/connections"));
175
+ result.apps = generated.map(({ app }) => app.name);
176
+ files.set("index.d.ts", renderIndex(generated, params.augment ?? DEFAULT_AUGMENT));
71
177
  const current = existsSync(outDir) ? new Set(readdirSync(outDir).filter((f) => f.endsWith(".d.ts"))) : /* @__PURE__ */ new Set();
72
178
  if (params.check) {
73
179
  for (const [file, content] of files) {
@@ -85,12 +191,13 @@ async function generateConnectionTypes(params) {
85
191
  for (const file of current) if (!files.has(file)) rmSync(join(outDir, file));
86
192
  return result;
87
193
  }
88
- var DEFAULT_ISSUER, DEFAULT_OUT_DIR, HEADER;
194
+ var DEFAULT_ISSUER, DEFAULT_OUT_DIR, DEFAULT_AUGMENT, HEADER;
89
195
  var init_typegen = __esm({
90
196
  "src/typegen.ts"() {
91
197
  "use strict";
92
198
  DEFAULT_ISSUER = "https://accounts-api.geekapps.com.br";
93
199
  DEFAULT_OUT_DIR = "geekapps-connections";
200
+ DEFAULT_AUGMENT = "@geekapps/auth-nextjs/connections";
94
201
  HEADER = "// Gerado pelo SDK do Geekapps Auth. N\xE3o edite \xE0 m\xE3o.\n\n";
95
202
  }
96
203
  });
@@ -112,15 +219,41 @@ var GEEKAPPS_AUTH_SERVER_URL = "https://accounts-api.geekapps.com.br";
112
219
  var AppConnectionError = class extends Error {
113
220
  status;
114
221
  code;
222
+ /** Código de erro como o servidor mandou (ex: "connection_inactive", "invalid_state"). */
223
+ reason;
224
+ /** Escopo que faltou, quando `code` é "missing_scope". */
225
+ scope;
115
226
  body;
116
- constructor(message, status, code, body) {
227
+ constructor(message, status, reason, body) {
117
228
  super(message);
118
229
  this.name = "AppConnectionError";
119
230
  this.status = status;
120
- this.code = code;
231
+ this.reason = reason;
232
+ this.code = toErrorCode(status, reason);
121
233
  this.body = body;
234
+ this.scope = body && typeof body === "object" && typeof body.scope === "string" ? body.scope : void 0;
122
235
  }
123
236
  };
237
+ function toErrorCode(status, reason) {
238
+ switch (reason) {
239
+ case "missing_scope":
240
+ case "app_connection_not_allowed":
241
+ return "missing_scope";
242
+ case "connection_expired":
243
+ return "connection_expired";
244
+ case "app_not_allowed":
245
+ return "app_not_allowed";
246
+ case "connection_inactive":
247
+ case "connection_revoked":
248
+ case "invalid_token":
249
+ case "membership_required":
250
+ return "connection_revoked";
251
+ case "network_error":
252
+ return "network_error";
253
+ }
254
+ if (status === 400 || status === 422) return "validation_error";
255
+ return "unknown";
256
+ }
124
257
  function env(name) {
125
258
  const value = process.env[name];
126
259
  return value && value.trim().length > 0 ? value : void 0;
@@ -154,6 +287,7 @@ async function connectApp(params) {
154
287
  if (!clientId) {
155
288
  throw new Error("App Connections: defina GEEKAPPS_AUTH_CLIENT_ID \u2014 \xE9 o client_id desta app, a que pede a conex\xE3o.");
156
289
  }
290
+ const targetAppId = params.targetAppId ?? await resolveAppSlug(params.app, params.config ?? {});
157
291
  const codeVerifier = randomBytes2(64).toString("base64url");
158
292
  const state = randomBytes2(24).toString("base64url");
159
293
  const store = await cookies2();
@@ -170,7 +304,10 @@ async function connectApp(params) {
170
304
  url.searchParams.set("state", state);
171
305
  url.searchParams.set("code_challenge", createHash2("sha256").update(codeVerifier).digest("base64url"));
172
306
  url.searchParams.set("code_challenge_method", "S256");
173
- url.searchParams.set("target_app_id", params.targetAppId);
307
+ url.searchParams.set("target_app_id", targetAppId);
308
+ if (params.scopes && params.scopes.length > 0) {
309
+ url.searchParams.set("connection_scopes", [...new Set(params.scopes)].join(" "));
310
+ }
174
311
  url.searchParams.set("prompt", "select_account consent");
175
312
  if (params.displayName) url.searchParams.set("connection_display_name", params.displayName);
176
313
  redirect2(url.toString());
@@ -222,6 +359,21 @@ async function completeAppConnection(params) {
222
359
  }
223
360
  };
224
361
  }
362
+ var _catalogCache = {
363
+ value: null,
364
+ fetchedAt: 0
365
+ };
366
+ async function resolveAppSlug(slug, config) {
367
+ if (!_catalogCache.value || Date.now() - _catalogCache.fetchedAt > 5 * 6e4) {
368
+ const { issuer, clientId, clientSecret } = requireCredentials(config);
369
+ const { fetchCatalog: fetchCatalog2 } = await Promise.resolve().then(() => (init_typegen(), typegen_exports));
370
+ _catalogCache.value = await fetchCatalog2({ issuer, clientId, clientSecret });
371
+ _catalogCache.fetchedAt = Date.now();
372
+ }
373
+ const app = _catalogCache.value.find((item) => item.slug === slug);
374
+ if (!app) throw new Error(`App Connections: a app "${slug}" n\xE3o est\xE1 no cat\xE1logo do Geekapps Auth.`);
375
+ return app.targetAppId;
376
+ }
225
377
  var _jwtCache = /* @__PURE__ */ new Map();
226
378
  async function postToAuthServer(path, connectionToken, config) {
227
379
  const { issuer, clientId, clientSecret } = requireCredentials(config);
@@ -309,29 +461,162 @@ async function rotateConnectionToken(params) {
309
461
  _jwtCache.delete(params.connectionToken);
310
462
  return { token: data.connection_token, expiresAt: data.expires_at };
311
463
  }
312
- var TYPES_AUGMENT_MODULE = "@geekapps/auth-nextjs/connections";
313
- async function syncTypes() {
314
- const { issuer, clientId, clientSecret } = resolveConfig({});
315
- if (!clientId || !clientSecret) return;
464
+ var MANIFEST_PATH = "/.well-known/geekapps-app.json";
465
+ var MANIFEST_TTL_MS = 5 * 6e4;
466
+ var _manifestCache = /* @__PURE__ */ new Map();
467
+ async function getRuntimeManifest(baseUrl, force = false) {
468
+ const url = `${baseUrl.replace(/\/+$/, "")}${MANIFEST_PATH}`;
469
+ const cached = _manifestCache.get(url);
470
+ if (cached && !force && Date.now() - cached.fetchedAt < MANIFEST_TTL_MS) return cached.value;
316
471
  try {
317
- const { generateConnectionTypes: generateConnectionTypes2 } = await Promise.resolve().then(() => (init_typegen(), typegen_exports));
318
- await generateConnectionTypes2({
319
- credentials: { issuer, clientId, clientSecret },
320
- augment: TYPES_AUGMENT_MODULE
321
- });
322
- } catch {
472
+ const response = await fetch(url, { headers: { accept: "application/json" }, cache: "no-store" });
473
+ if (!response.ok) throw new Error(`status ${response.status}`);
474
+ const value = await response.json();
475
+ if (value?.version !== 1 || !Array.isArray(value.routes)) throw new Error("formato inv\xE1lido");
476
+ _manifestCache.set(url, { value, fetchedAt: Date.now() });
477
+ return value;
478
+ } catch (error) {
479
+ if (cached) return cached.value;
480
+ throw new AppConnectionError(
481
+ `App Connections: manifesto da app alvo indispon\xEDvel em ${url} (${error.message})`,
482
+ 0,
483
+ "network_error",
484
+ null
485
+ );
486
+ }
487
+ }
488
+ function propertyNames(schema) {
489
+ const properties2 = schema?.properties;
490
+ return properties2 && typeof properties2 === "object" ? Object.keys(properties2) : [];
491
+ }
492
+ function appendQuery(url, key, value) {
493
+ if (value === void 0) return;
494
+ if (Array.isArray(value)) {
495
+ for (const item of value) appendQuery(url, key, item);
496
+ return;
497
+ }
498
+ url.searchParams.append(key, value !== null && typeof value === "object" ? JSON.stringify(value) : String(value));
499
+ }
500
+ function buildRequest(baseUrl, route, input) {
501
+ const rest = { ...input };
502
+ const path = route.path.replace(/:([A-Za-z0-9_]+)/g, (_, name) => {
503
+ const value = rest[name];
504
+ delete rest[name];
505
+ if (value === void 0 || value === null || value === "") {
506
+ throw new AppConnectionError(`App Connections: falta o par\xE2metro "${name}" em ${route.route}.`, 0, "validation_error", null);
507
+ }
508
+ return encodeURIComponent(String(value));
509
+ });
510
+ const url = new URL(path.replace(/^\//, ""), `${baseUrl.replace(/\/+$/, "")}/`);
511
+ const method = route.method.toUpperCase();
512
+ const bodyless = method === "GET" || method === "DELETE" || method === "HEAD";
513
+ const queryFields = new Set(propertyNames(route.query));
514
+ const body = {};
515
+ for (const [key, value] of Object.entries(rest)) {
516
+ if (bodyless || queryFields.has(key)) appendQuery(url, key, value);
517
+ else if (value !== void 0) body[key] = value;
518
+ }
519
+ const hasBody = !bodyless && (route.body !== null || Object.keys(body).length > 0);
520
+ return { url, method, body: hasBody ? JSON.stringify(body) : void 0 };
521
+ }
522
+ async function accessTokenFor(connectionToken, config) {
523
+ try {
524
+ return await getConnectionAccessToken({ connectionToken, config });
525
+ } catch (error) {
526
+ if (error instanceof AppConnectionError) throw error;
527
+ throw new AppConnectionError(`App Connections: auth-server inacess\xEDvel (${error.message})`, 0, "network_error", null);
528
+ }
529
+ }
530
+ async function callRoute(connectionToken, options, routeName, input = {}) {
531
+ const config = options.config ?? {};
532
+ let token = await accessTokenFor(connectionToken, config);
533
+ const baseUrl = options.baseUrl ?? token.targetApiBaseUrl;
534
+ if (!baseUrl) {
535
+ throw new AppConnectionError("App Connections: a app alvo n\xE3o declarou apiBaseUrl \u2014 passe `baseUrl`.", 0, "unknown", null);
536
+ }
537
+ let manifest = await getRuntimeManifest(baseUrl);
538
+ let route = manifest.routes.find((item) => item.route === routeName);
539
+ if (!route) {
540
+ manifest = await getRuntimeManifest(baseUrl, true);
541
+ route = manifest.routes.find((item) => item.route === routeName);
323
542
  }
543
+ if (!route) {
544
+ throw new AppConnectionError(`App Connections: a app alvo n\xE3o tem a rota "${routeName}".`, 404, "route_not_found", null);
545
+ }
546
+ const request = buildRequest(baseUrl, route, input);
547
+ for (let attempt = 0; ; attempt++) {
548
+ let response;
549
+ try {
550
+ response = await fetch(request.url, {
551
+ method: request.method,
552
+ headers: {
553
+ authorization: `Bearer ${token.accessToken}`,
554
+ accept: "application/json",
555
+ ...request.body !== void 0 && { "content-type": "application/json" }
556
+ },
557
+ body: request.body,
558
+ cache: "no-store"
559
+ });
560
+ } catch (error) {
561
+ throw new AppConnectionError(`App Connections: app alvo inacess\xEDvel (${error.message})`, 0, "network_error", null);
562
+ }
563
+ if (response.status === 401 && attempt === 0) {
564
+ _jwtCache.delete(connectionToken);
565
+ token = await accessTokenFor(connectionToken, config);
566
+ continue;
567
+ }
568
+ const text = await response.text();
569
+ let body = null;
570
+ try {
571
+ body = text ? JSON.parse(text) : null;
572
+ } catch {
573
+ body = text;
574
+ }
575
+ if (!response.ok) {
576
+ const reason = errorCode(body) ?? (response.status === 401 ? "connection_revoked" : void 0);
577
+ throw new AppConnectionError(
578
+ `App Connections: ${route.route} falhou (${response.status}${reason ? ` ${reason}` : ""})`,
579
+ response.status,
580
+ reason,
581
+ body
582
+ );
583
+ }
584
+ return body ?? void 0;
585
+ }
586
+ }
587
+ function routeProxy(call, path) {
588
+ return new Proxy(function() {
589
+ }, {
590
+ get(_target, property) {
591
+ if (typeof property !== "string" || property === "then") return void 0;
592
+ return routeProxy(call, [...path, property]);
593
+ },
594
+ apply(_target, _this, args) {
595
+ if (path.length === 0) throw new Error("App Connections: chame uma rota, ex: post.messages.send({ ... }).");
596
+ return call(path.join("."), args[0]);
597
+ }
598
+ });
324
599
  }
325
- if (process.env.NODE_ENV === "development" && process.env.GEEKAPPS_CONNECTIONS_TYPES !== "off") {
326
- void syncTypes();
327
- setInterval(() => void syncTypes(), 6e4).unref?.();
600
+ function createConnections(config = {}) {
601
+ return new Proxy({}, {
602
+ get(_target, slug) {
603
+ if (typeof slug !== "string" || slug === "then") return void 0;
604
+ return (connectionToken, options = {}) => routeProxy(
605
+ (route, input) => callRoute(connectionToken, { config, ...options }, route, input),
606
+ []
607
+ );
608
+ }
609
+ });
328
610
  }
611
+ var connections = createConnections();
329
612
  export {
330
613
  AppConnectionError,
331
614
  completeAppConnection,
332
615
  connectApp,
333
616
  connectionFetch,
617
+ connections,
334
618
  createConnectionClient,
619
+ createConnections,
335
620
  getConnectionAccessToken,
336
621
  rotateConnectionToken
337
622
  };