@geekapps/auth-nextjs 0.21.0 → 0.24.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,7 +14,9 @@ __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";
@@ -28,13 +30,95 @@ async function fetchCatalog({ issuer, clientId, clientSecret }) {
28
30
  }
29
31
  return await response.json();
30
32
  }
33
+ async function fetchManifest(manifestUrl) {
34
+ const response = await fetch(manifestUrl, { headers: { accept: "application/json" }, cache: "no-store" });
35
+ if (!response.ok) throw new Error(`manifesto indispon\xEDvel (${response.status})`);
36
+ const manifest = await response.json();
37
+ if (manifest?.version !== 1 || !manifest.scopes || !Array.isArray(manifest.routes)) {
38
+ throw new Error("manifesto em formato inv\xE1lido");
39
+ }
40
+ return manifest;
41
+ }
42
+ function pascal(name) {
43
+ return name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join("");
44
+ }
45
+ function properties(schema) {
46
+ return schema?.properties ?? {};
47
+ }
48
+ function required(schema) {
49
+ return Array.isArray(schema?.required) ? schema.required : [];
50
+ }
51
+ function mergeInputSchema(route) {
52
+ const parts = [route.params, route.query, route.body];
53
+ const pathParams = [...route.path.matchAll(/:([A-Za-z0-9_]+)/g)].map((m) => m[1]);
54
+ return {
55
+ type: "object",
56
+ properties: Object.assign({}, ...parts.map(properties)),
57
+ required: [.../* @__PURE__ */ new Set([...pathParams, ...parts.flatMap(required)])],
58
+ additionalProperties: false
59
+ };
60
+ }
61
+ async function generateManifestTypes(app, manifest) {
62
+ const { compile } = await import("json-schema-to-typescript");
63
+ const options = { bannerComment: "", additionalProperties: false, format: false, declareExternallyReferenced: true };
64
+ const blocks = [];
65
+ const routeEntries = [];
66
+ for (const route of manifest.routes) {
67
+ const base = pascal(route.route);
68
+ const input = `${base}Input`;
69
+ const output = `${base}Output`;
70
+ blocks.push(await compile(mergeInputSchema(route), input, options));
71
+ blocks.push(
72
+ route.response ? await compile({ ...route.response, title: void 0 }, output, options) : `export type ${output} = unknown;
73
+ `
74
+ );
75
+ routeEntries.push(
76
+ ` ${JSON.stringify(route.route)}: {
77
+ scope: ${JSON.stringify(route.scope)};
78
+ method: ${JSON.stringify(route.method)};
79
+ path: ${JSON.stringify(route.path)};
80
+ input: ${input};
81
+ output: ${output};
82
+ };`
83
+ );
84
+ }
85
+ const scopeIds = Object.keys(manifest.scopes);
86
+ const scopeDocs = scopeIds.map((id) => ` ${JSON.stringify(id)}: ${JSON.stringify(manifest.scopes[id].description)};`).join("\n");
87
+ return `${HEADER}/** Escopos que esta app aceita em conex\xF5es. */
88
+ export type Scope = ${scopeIds.length ? scopeIds.map((id) => JSON.stringify(id)).join(" | ") : "never"};
89
+
90
+ /** Descri\xE7\xE3o de cada escopo, como aparece no consent. */
91
+ export interface ScopeDescriptions {
92
+ ${scopeDocs}
93
+ }
94
+
95
+ /** Rotas liberadas por escopo, pelo nome usado no cliente (\`post.messages.send(...)\`). */
96
+ export interface Routes {
97
+ ${routeEntries.join("\n")}
98
+ }
99
+
100
+ export interface App {
101
+ slug: ${JSON.stringify(app.slug)};
102
+ targetAppId: ${JSON.stringify(app.targetAppId)};
103
+ scopes: Scope;
104
+ routes: Routes;
105
+ }
106
+
107
+ ${blocks.join("\n")}`;
108
+ }
31
109
  async function generateOpenApiTypes(openApiUrl) {
32
110
  const { default: openapiTS, astToString } = await import("openapi-typescript");
33
111
  return HEADER + astToString(await openapiTS(new URL(openApiUrl)));
34
112
  }
35
113
  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");
114
+ const imports = apps.flatMap(({ app, manifest, openApi }, i) => [
115
+ manifest ? `import type { App as App${i} } from "./${app.slug}";` : null,
116
+ openApi ? `import type { paths as paths${i} } from "./${app.slug}.openapi";` : null
117
+ ]).filter(Boolean).join("\n");
118
+ const members = apps.map(({ app, manifest, openApi }, i) => {
119
+ const fields = [manifest ? `app: App${i};` : null, openApi ? `paths: paths${i};` : null].filter(Boolean).join(" ");
120
+ return ` ${JSON.stringify(app.slug)}: { ${fields} };`;
121
+ }).join("\n");
38
122
  return `${HEADER}${imports}
39
123
 
40
124
  declare module ${JSON.stringify(augment)} {
@@ -52,22 +136,38 @@ async function generateConnectionTypes(params) {
52
136
  const files = /* @__PURE__ */ new Map();
53
137
  const result = { apps: [], failed: [], stale: [] };
54
138
  const generated = [];
139
+ const keepExisting = (file) => {
140
+ const existing = join(outDir, file);
141
+ if (!existsSync(existing)) return false;
142
+ files.set(file, readFileSync(existing, "utf-8"));
143
+ return true;
144
+ };
55
145
  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);
146
+ const entry = { app, manifest: false, openApi: false };
147
+ if (app.manifestUrl) {
148
+ const file = `${app.slug}.d.ts`;
149
+ try {
150
+ files.set(file, await generateManifestTypes(app, await fetchManifest(app.manifestUrl)));
151
+ entry.manifest = true;
152
+ } catch (error) {
153
+ result.failed.push({ name: app.name, reason: `manifesto: ${error.message}` });
154
+ entry.manifest = keepExisting(file);
155
+ }
156
+ }
157
+ if (app.openApiUrl) {
158
+ const file = `${app.slug}.openapi.d.ts`;
159
+ try {
160
+ files.set(file, await generateOpenApiTypes(app.openApiUrl));
161
+ entry.openApi = true;
162
+ } catch (error) {
163
+ result.failed.push({ name: app.name, reason: `openapi: ${error.message}` });
164
+ entry.openApi = keepExisting(file);
66
165
  }
67
166
  }
167
+ if (entry.manifest || entry.openApi) generated.push(entry);
68
168
  }
69
- result.apps = generated.map((app) => app.name);
70
- files.set("index.d.ts", renderIndex(generated, params.augment ?? "@geekapps/auth-nextjs/connections"));
169
+ result.apps = generated.map(({ app }) => app.name);
170
+ files.set("index.d.ts", renderIndex(generated, params.augment ?? DEFAULT_AUGMENT));
71
171
  const current = existsSync(outDir) ? new Set(readdirSync(outDir).filter((f) => f.endsWith(".d.ts"))) : /* @__PURE__ */ new Set();
72
172
  if (params.check) {
73
173
  for (const [file, content] of files) {
@@ -85,12 +185,13 @@ async function generateConnectionTypes(params) {
85
185
  for (const file of current) if (!files.has(file)) rmSync(join(outDir, file));
86
186
  return result;
87
187
  }
88
- var DEFAULT_ISSUER, DEFAULT_OUT_DIR, HEADER;
188
+ var DEFAULT_ISSUER, DEFAULT_OUT_DIR, DEFAULT_AUGMENT, HEADER;
89
189
  var init_typegen = __esm({
90
190
  "src/typegen.ts"() {
91
191
  "use strict";
92
192
  DEFAULT_ISSUER = "https://accounts-api.geekapps.com.br";
93
193
  DEFAULT_OUT_DIR = "geekapps-connections";
194
+ DEFAULT_AUGMENT = "@geekapps/auth-nextjs/connections";
94
195
  HEADER = "// Gerado pelo SDK do Geekapps Auth. N\xE3o edite \xE0 m\xE3o.\n\n";
95
196
  }
96
197
  });
@@ -112,15 +213,39 @@ var GEEKAPPS_AUTH_SERVER_URL = "https://accounts-api.geekapps.com.br";
112
213
  var AppConnectionError = class extends Error {
113
214
  status;
114
215
  code;
216
+ /** Código de erro como o servidor mandou (ex: "connection_inactive", "invalid_state"). */
217
+ reason;
218
+ /** Escopo que faltou, quando `code` é "missing_scope". */
219
+ scope;
115
220
  body;
116
- constructor(message, status, code, body) {
221
+ constructor(message, status, reason, body) {
117
222
  super(message);
118
223
  this.name = "AppConnectionError";
119
224
  this.status = status;
120
- this.code = code;
225
+ this.reason = reason;
226
+ this.code = toErrorCode(status, reason);
121
227
  this.body = body;
228
+ this.scope = body && typeof body === "object" && typeof body.scope === "string" ? body.scope : void 0;
122
229
  }
123
230
  };
231
+ function toErrorCode(status, reason) {
232
+ switch (reason) {
233
+ case "missing_scope":
234
+ case "app_connection_not_allowed":
235
+ return "missing_scope";
236
+ case "connection_expired":
237
+ return "connection_expired";
238
+ case "connection_inactive":
239
+ case "connection_revoked":
240
+ case "invalid_token":
241
+ case "membership_required":
242
+ return "connection_revoked";
243
+ case "network_error":
244
+ return "network_error";
245
+ }
246
+ if (status === 400 || status === 422) return "validation_error";
247
+ return "unknown";
248
+ }
124
249
  function env(name) {
125
250
  const value = process.env[name];
126
251
  return value && value.trim().length > 0 ? value : void 0;
@@ -154,6 +279,7 @@ async function connectApp(params) {
154
279
  if (!clientId) {
155
280
  throw new Error("App Connections: defina GEEKAPPS_AUTH_CLIENT_ID \u2014 \xE9 o client_id desta app, a que pede a conex\xE3o.");
156
281
  }
282
+ const targetAppId = params.targetAppId ?? await resolveAppSlug(params.app, params.config ?? {});
157
283
  const codeVerifier = randomBytes2(64).toString("base64url");
158
284
  const state = randomBytes2(24).toString("base64url");
159
285
  const store = await cookies2();
@@ -170,7 +296,10 @@ async function connectApp(params) {
170
296
  url.searchParams.set("state", state);
171
297
  url.searchParams.set("code_challenge", createHash2("sha256").update(codeVerifier).digest("base64url"));
172
298
  url.searchParams.set("code_challenge_method", "S256");
173
- url.searchParams.set("target_app_id", params.targetAppId);
299
+ url.searchParams.set("target_app_id", targetAppId);
300
+ if (params.scopes && params.scopes.length > 0) {
301
+ url.searchParams.set("connection_scopes", [...new Set(params.scopes)].join(" "));
302
+ }
174
303
  url.searchParams.set("prompt", "select_account consent");
175
304
  if (params.displayName) url.searchParams.set("connection_display_name", params.displayName);
176
305
  redirect2(url.toString());
@@ -222,6 +351,21 @@ async function completeAppConnection(params) {
222
351
  }
223
352
  };
224
353
  }
354
+ var _catalogCache = {
355
+ value: null,
356
+ fetchedAt: 0
357
+ };
358
+ async function resolveAppSlug(slug, config) {
359
+ if (!_catalogCache.value || Date.now() - _catalogCache.fetchedAt > 5 * 6e4) {
360
+ const { issuer, clientId, clientSecret } = requireCredentials(config);
361
+ const { fetchCatalog: fetchCatalog2 } = await Promise.resolve().then(() => (init_typegen(), typegen_exports));
362
+ _catalogCache.value = await fetchCatalog2({ issuer, clientId, clientSecret });
363
+ _catalogCache.fetchedAt = Date.now();
364
+ }
365
+ const app = _catalogCache.value.find((item) => item.slug === slug);
366
+ if (!app) throw new Error(`App Connections: a app "${slug}" n\xE3o est\xE1 no cat\xE1logo do Geekapps Auth.`);
367
+ return app.targetAppId;
368
+ }
225
369
  var _jwtCache = /* @__PURE__ */ new Map();
226
370
  async function postToAuthServer(path, connectionToken, config) {
227
371
  const { issuer, clientId, clientSecret } = requireCredentials(config);
@@ -309,6 +453,154 @@ async function rotateConnectionToken(params) {
309
453
  _jwtCache.delete(params.connectionToken);
310
454
  return { token: data.connection_token, expiresAt: data.expires_at };
311
455
  }
456
+ var MANIFEST_PATH = "/.well-known/geekapps-app.json";
457
+ var MANIFEST_TTL_MS = 5 * 6e4;
458
+ var _manifestCache = /* @__PURE__ */ new Map();
459
+ async function getRuntimeManifest(baseUrl, force = false) {
460
+ const url = `${baseUrl.replace(/\/+$/, "")}${MANIFEST_PATH}`;
461
+ const cached = _manifestCache.get(url);
462
+ if (cached && !force && Date.now() - cached.fetchedAt < MANIFEST_TTL_MS) return cached.value;
463
+ try {
464
+ const response = await fetch(url, { headers: { accept: "application/json" }, cache: "no-store" });
465
+ if (!response.ok) throw new Error(`status ${response.status}`);
466
+ const value = await response.json();
467
+ if (value?.version !== 1 || !Array.isArray(value.routes)) throw new Error("formato inv\xE1lido");
468
+ _manifestCache.set(url, { value, fetchedAt: Date.now() });
469
+ return value;
470
+ } catch (error) {
471
+ if (cached) return cached.value;
472
+ throw new AppConnectionError(
473
+ `App Connections: manifesto da app alvo indispon\xEDvel em ${url} (${error.message})`,
474
+ 0,
475
+ "network_error",
476
+ null
477
+ );
478
+ }
479
+ }
480
+ function propertyNames(schema) {
481
+ const properties2 = schema?.properties;
482
+ return properties2 && typeof properties2 === "object" ? Object.keys(properties2) : [];
483
+ }
484
+ function appendQuery(url, key, value) {
485
+ if (value === void 0) return;
486
+ if (Array.isArray(value)) {
487
+ for (const item of value) appendQuery(url, key, item);
488
+ return;
489
+ }
490
+ url.searchParams.append(key, value !== null && typeof value === "object" ? JSON.stringify(value) : String(value));
491
+ }
492
+ function buildRequest(baseUrl, route, input) {
493
+ const rest = { ...input };
494
+ const path = route.path.replace(/:([A-Za-z0-9_]+)/g, (_, name) => {
495
+ const value = rest[name];
496
+ delete rest[name];
497
+ if (value === void 0 || value === null || value === "") {
498
+ throw new AppConnectionError(`App Connections: falta o par\xE2metro "${name}" em ${route.route}.`, 0, "validation_error", null);
499
+ }
500
+ return encodeURIComponent(String(value));
501
+ });
502
+ const url = new URL(path.replace(/^\//, ""), `${baseUrl.replace(/\/+$/, "")}/`);
503
+ const method = route.method.toUpperCase();
504
+ const bodyless = method === "GET" || method === "DELETE" || method === "HEAD";
505
+ const queryFields = new Set(propertyNames(route.query));
506
+ const body = {};
507
+ for (const [key, value] of Object.entries(rest)) {
508
+ if (bodyless || queryFields.has(key)) appendQuery(url, key, value);
509
+ else if (value !== void 0) body[key] = value;
510
+ }
511
+ const hasBody = !bodyless && (route.body !== null || Object.keys(body).length > 0);
512
+ return { url, method, body: hasBody ? JSON.stringify(body) : void 0 };
513
+ }
514
+ async function accessTokenFor(connectionToken, config) {
515
+ try {
516
+ return await getConnectionAccessToken({ connectionToken, config });
517
+ } catch (error) {
518
+ if (error instanceof AppConnectionError) throw error;
519
+ throw new AppConnectionError(`App Connections: auth-server inacess\xEDvel (${error.message})`, 0, "network_error", null);
520
+ }
521
+ }
522
+ async function callRoute(connectionToken, options, routeName, input = {}) {
523
+ const config = options.config ?? {};
524
+ let token = await accessTokenFor(connectionToken, config);
525
+ const baseUrl = options.baseUrl ?? token.targetApiBaseUrl;
526
+ if (!baseUrl) {
527
+ throw new AppConnectionError("App Connections: a app alvo n\xE3o declarou apiBaseUrl \u2014 passe `baseUrl`.", 0, "unknown", null);
528
+ }
529
+ let manifest = await getRuntimeManifest(baseUrl);
530
+ let route = manifest.routes.find((item) => item.route === routeName);
531
+ if (!route) {
532
+ manifest = await getRuntimeManifest(baseUrl, true);
533
+ route = manifest.routes.find((item) => item.route === routeName);
534
+ }
535
+ if (!route) {
536
+ throw new AppConnectionError(`App Connections: a app alvo n\xE3o tem a rota "${routeName}".`, 404, "route_not_found", null);
537
+ }
538
+ const request = buildRequest(baseUrl, route, input);
539
+ for (let attempt = 0; ; attempt++) {
540
+ let response;
541
+ try {
542
+ response = await fetch(request.url, {
543
+ method: request.method,
544
+ headers: {
545
+ authorization: `Bearer ${token.accessToken}`,
546
+ accept: "application/json",
547
+ ...request.body !== void 0 && { "content-type": "application/json" }
548
+ },
549
+ body: request.body,
550
+ cache: "no-store"
551
+ });
552
+ } catch (error) {
553
+ throw new AppConnectionError(`App Connections: app alvo inacess\xEDvel (${error.message})`, 0, "network_error", null);
554
+ }
555
+ if (response.status === 401 && attempt === 0) {
556
+ _jwtCache.delete(connectionToken);
557
+ token = await accessTokenFor(connectionToken, config);
558
+ continue;
559
+ }
560
+ const text = await response.text();
561
+ let body = null;
562
+ try {
563
+ body = text ? JSON.parse(text) : null;
564
+ } catch {
565
+ body = text;
566
+ }
567
+ if (!response.ok) {
568
+ const reason = errorCode(body) ?? (response.status === 401 ? "connection_revoked" : void 0);
569
+ throw new AppConnectionError(
570
+ `App Connections: ${route.route} falhou (${response.status}${reason ? ` ${reason}` : ""})`,
571
+ response.status,
572
+ reason,
573
+ body
574
+ );
575
+ }
576
+ return body ?? void 0;
577
+ }
578
+ }
579
+ function routeProxy(call, path) {
580
+ return new Proxy(function() {
581
+ }, {
582
+ get(_target, property) {
583
+ if (typeof property !== "string" || property === "then") return void 0;
584
+ return routeProxy(call, [...path, property]);
585
+ },
586
+ apply(_target, _this, args) {
587
+ if (path.length === 0) throw new Error("App Connections: chame uma rota, ex: post.messages.send({ ... }).");
588
+ return call(path.join("."), args[0]);
589
+ }
590
+ });
591
+ }
592
+ function createConnections(config = {}) {
593
+ return new Proxy({}, {
594
+ get(_target, slug) {
595
+ if (typeof slug !== "string" || slug === "then") return void 0;
596
+ return (connectionToken, options = {}) => routeProxy(
597
+ (route, input) => callRoute(connectionToken, { config, ...options }, route, input),
598
+ []
599
+ );
600
+ }
601
+ });
602
+ }
603
+ var connections = createConnections();
312
604
  var TYPES_AUGMENT_MODULE = "@geekapps/auth-nextjs/connections";
313
605
  async function syncTypes() {
314
606
  const { issuer, clientId, clientSecret } = resolveConfig({});
@@ -331,7 +623,9 @@ export {
331
623
  completeAppConnection,
332
624
  connectApp,
333
625
  connectionFetch,
626
+ connections,
334
627
  createConnectionClient,
628
+ createConnections,
335
629
  getConnectionAccessToken,
336
630
  rotateConnectionToken
337
631
  };