@rolino/local-auth 0.5.0-beta.0 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @rolino/local-auth
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a56f834: Replace custom interactive CLI authorization with exact-resource OAuth, add the stateless MCP 2026-07-28 transport foundation, and provide safe connection instructions for workspace discovery and confirmed actions.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [d162f85]
12
+ - Updated dependencies [50cb9fc]
13
+ - Updated dependencies [1ac7ed5]
14
+ - Updated dependencies [a56f834]
15
+ - @rolino/sdk@0.6.0
16
+
17
+ ## 0.5.0
18
+
19
+ ### Minor Changes
20
+
21
+ - 1edd1e7: Add published Blog Studio delivery contracts and the server-only Next.js 16 Blog Studio package.
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies [4f9e790]
26
+ - Updated dependencies [1edd1e7]
27
+ - Updated dependencies [74a13ea]
28
+ - Updated dependencies [4af607d]
29
+ - Updated dependencies [4a893dd]
30
+ - Updated dependencies [843a1db]
31
+ - @rolino/sdk@0.5.0
32
+
3
33
  ## 0.5.0-beta.0
4
34
 
5
35
  ### Minor Changes
package/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # `@rolino/local-auth`
2
2
 
3
- Node-only, host-scoped credential storage shared by Rolino's CLI and local
4
- stdio MCP adapter. Environment credentials take precedence over the portable
5
- owner-only file store.
3
+ Node-only OAuth token storage shared by Rolino's CLI and local stdio MCP
4
+ adapter. Token sets are scoped by host, issuer, client, and exact resource.
5
+ The store refreshes short-lived access tokens under one rotation lock and
6
+ writes the owner-only file atomically. Environment API keys still take
7
+ precedence for headless automation. The removed interactive credential is
8
+ never read or converted.
6
9
 
7
10
  This package must not be used by the browser, public SDK, application routes,
8
11
  or server domain services. It is published only as a supported transitive
package/dist/index.cjs CHANGED
@@ -20,98 +20,179 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- clearStoredCredential: () => clearStoredCredential,
24
- credentialStorePath: () => credentialStorePath,
25
- loadStoredCredential: () => loadStoredCredential,
26
- resolveCredential: () => resolveCredential,
27
- saveStoredCredential: () => saveStoredCredential
23
+ ROLINO_CLI_OAUTH_CLIENT_ID: () => ROLINO_CLI_OAUTH_CLIENT_ID,
24
+ clearOAuthTokenSet: () => clearOAuthTokenSet,
25
+ createOAuthAccessTokenProvider: () => createOAuthAccessTokenProvider,
26
+ loadOAuthTokenSet: () => loadOAuthTokenSet,
27
+ oauthConfiguration: () => oauthConfiguration,
28
+ oauthTokenStorePath: () => oauthTokenStorePath,
29
+ resolveLocalAuthentication: () => resolveLocalAuthentication,
30
+ saveOAuthTokenSet: () => saveOAuthTokenSet
28
31
  });
29
32
  module.exports = __toCommonJS(index_exports);
30
33
  var import_node_fs = require("fs");
31
34
  var import_node_os = require("os");
32
35
  var import_node_path = require("path");
33
36
  var import_sdk = require("@rolino/sdk");
37
+ var ROLINO_CLI_OAUTH_CLIENT_ID = "rolino-cli";
38
+ var refreshLocks = /* @__PURE__ */ new Map();
34
39
  function defaultConfigDirectory(env) {
35
40
  if (env.ROLINO_CONFIG_DIR) return env.ROLINO_CONFIG_DIR;
36
- if (process.platform === "win32" && env.APPDATA) {
37
- return (0, import_node_path.join)(env.APPDATA, "Rolino");
38
- }
39
- if (process.platform === "darwin") {
40
- return (0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Application Support", "Rolino");
41
- }
41
+ if (process.platform === "win32" && env.APPDATA) return (0, import_node_path.join)(env.APPDATA, "Rolino");
42
+ if (process.platform === "darwin") return (0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Application Support", "Rolino");
42
43
  return (0, import_node_path.join)(env.XDG_CONFIG_HOME ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".config"), "rolino");
43
44
  }
44
- function credentialStorePath(env = process.env) {
45
- return (0, import_node_path.join)(defaultConfigDirectory(env), "credentials.json");
45
+ function oauthTokenStorePath(env = process.env) {
46
+ return (0, import_node_path.join)(defaultConfigDirectory(env), "oauth-tokens.json");
47
+ }
48
+ function oauthConfiguration(baseUrl) {
49
+ const host = (0, import_sdk.normalizeRolinoBaseUrl)(baseUrl);
50
+ return {
51
+ host,
52
+ issuer: `${host}/api/auth`,
53
+ clientId: ROLINO_CLI_OAUTH_CLIENT_ID,
54
+ resource: `${host}/api/v1`
55
+ };
46
56
  }
47
- function isStoredCredential(value) {
57
+ function tokenKey(input) {
58
+ return JSON.stringify([input.host, input.issuer, input.clientId, input.resource]);
59
+ }
60
+ function isTokenSet(value) {
48
61
  if (!value || typeof value !== "object") return false;
49
- const credential = value;
50
- return typeof credential.token === "string" && credential.token.length > 0 && typeof credential.createdAt === "string" && typeof credential.expiresAt === "string" && Boolean(credential.organization) && typeof credential.organization?.id === "string" && typeof credential.organization?.name === "string";
62
+ const token = value;
63
+ return [
64
+ token.host,
65
+ token.issuer,
66
+ token.clientId,
67
+ token.resource,
68
+ token.accessToken,
69
+ token.accessTokenExpiresAt,
70
+ token.refreshToken,
71
+ token.refreshTokenExpiresAt
72
+ ].every((item) => typeof item === "string" && item.length > 0) && Array.isArray(token.scopes) && token.scopes.every((scope) => typeof scope === "string");
51
73
  }
52
74
  function readStore(env) {
53
- const path = credentialStorePath(env);
54
- if (!(0, import_node_fs.existsSync)(path)) return { version: 1, credentials: {} };
55
- let parsed;
75
+ const path = oauthTokenStorePath(env);
76
+ if (!(0, import_node_fs.existsSync)(path)) return { version: 2, tokens: {} };
56
77
  try {
57
- parsed = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
78
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
79
+ if (parsed.version !== 2 || !parsed.tokens || typeof parsed.tokens !== "object" || !Object.values(parsed.tokens).every(isTokenSet)) throw new Error("invalid");
80
+ return parsed;
58
81
  } catch {
59
- throw new TypeError(`Rolino could not read its credential store at ${path}.`);
60
- }
61
- if (!parsed || typeof parsed !== "object") {
62
- throw new TypeError(`Rolino's credential store at ${path} is invalid.`);
82
+ throw new TypeError(`Rolino could not read its OAuth token store at ${path}.`);
63
83
  }
64
- const candidate = parsed;
65
- if (candidate.version !== 1 || !candidate.credentials || typeof candidate.credentials !== "object" || !Object.values(candidate.credentials).every(isStoredCredential)) {
66
- throw new TypeError(`Rolino's credential store at ${path} is invalid.`);
67
- }
68
- return candidate;
69
84
  }
70
85
  function writeStore(store, env) {
71
- const path = credentialStorePath(env);
86
+ const path = oauthTokenStorePath(env);
87
+ const temporary = `${path}.${process.pid}.tmp`;
72
88
  (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true, mode: 448 });
73
- (0, import_node_fs.writeFileSync)(path, `${JSON.stringify(store, null, 2)}
89
+ (0, import_node_fs.writeFileSync)(temporary, `${JSON.stringify(store, null, 2)}
74
90
  `, {
75
91
  encoding: "utf8",
76
92
  mode: 384
77
93
  });
94
+ (0, import_node_fs.chmodSync)(temporary, 384);
95
+ (0, import_node_fs.renameSync)(temporary, path);
78
96
  (0, import_node_fs.chmodSync)(path, 384);
97
+ return path;
79
98
  }
80
- function loadStoredCredential(baseUrl, env = process.env) {
81
- return readStore(env).credentials[(0, import_sdk.normalizeRolinoBaseUrl)(baseUrl)] ?? null;
99
+ function loadOAuthTokenSet(baseUrl, env = process.env) {
100
+ const configuration = oauthConfiguration(baseUrl);
101
+ return readStore(env).tokens[tokenKey(configuration)] ?? null;
82
102
  }
83
- function saveStoredCredential(baseUrl, credential, env = process.env) {
103
+ function saveOAuthTokenSet(tokenSet, env = process.env) {
104
+ const configuration = oauthConfiguration(tokenSet.host);
105
+ if (tokenSet.host !== configuration.host || tokenSet.issuer !== configuration.issuer || tokenSet.clientId !== configuration.clientId || tokenSet.resource !== configuration.resource) throw new TypeError("The OAuth token set does not match the Rolino host.");
84
106
  const store = readStore(env);
85
- store.credentials[(0, import_sdk.normalizeRolinoBaseUrl)(baseUrl)] = credential;
86
- writeStore(store, env);
87
- return credentialStorePath(env);
107
+ store.tokens[tokenKey(tokenSet)] = tokenSet;
108
+ const path = writeStore(store, env);
109
+ const retiredInteractiveCredential = (0, import_node_path.join)(defaultConfigDirectory(env), "credentials.json");
110
+ if ((0, import_node_fs.existsSync)(retiredInteractiveCredential)) (0, import_node_fs.unlinkSync)(retiredInteractiveCredential);
111
+ return path;
88
112
  }
89
- function clearStoredCredential(baseUrl, env = process.env) {
90
- const path = credentialStorePath(env);
113
+ function clearOAuthTokenSet(baseUrl, env = process.env) {
114
+ const path = oauthTokenStorePath(env);
91
115
  const store = readStore(env);
92
- const key = (0, import_sdk.normalizeRolinoBaseUrl)(baseUrl);
93
- const existed = Boolean(store.credentials[key]);
94
- delete store.credentials[key];
95
- if (Object.keys(store.credentials).length === 0) {
116
+ const key = tokenKey(oauthConfiguration(baseUrl));
117
+ const existed = Boolean(store.tokens[key]);
118
+ delete store.tokens[key];
119
+ if (!Object.keys(store.tokens).length) {
96
120
  if ((0, import_node_fs.existsSync)(path)) (0, import_node_fs.unlinkSync)(path);
97
121
  } else {
98
122
  writeStore(store, env);
99
123
  }
100
124
  return existed;
101
125
  }
102
- function resolveCredential(baseUrl, env = process.env) {
103
- if (env.ROLINO_TOKEN) {
104
- return { source: "environment", token: env.ROLINO_TOKEN };
105
- }
106
- const stored = loadStoredCredential(baseUrl, env);
107
- return stored ? { source: "stored", token: stored.token, credential: stored } : { source: "none", token: null };
126
+ async function refreshOAuthTokenSet(options) {
127
+ const current = loadOAuthTokenSet(options.baseUrl, options.env);
128
+ if (!current) throw new TypeError("OAuth sign-in is required. Run `rolino auth login`.");
129
+ const key = tokenKey(current);
130
+ const existing = refreshLocks.get(key);
131
+ if (existing) return existing;
132
+ const refresh = (async () => {
133
+ const body = new URLSearchParams({
134
+ grant_type: "refresh_token",
135
+ refresh_token: current.refreshToken,
136
+ client_id: current.clientId,
137
+ resource: current.resource
138
+ });
139
+ const response = await options.fetch(`${current.issuer}/oauth2/token`, {
140
+ method: "POST",
141
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
142
+ body
143
+ });
144
+ const payload = await response.json().catch(() => null);
145
+ if (!response.ok || !payload || typeof payload.access_token !== "string") {
146
+ throw new TypeError("OAuth sign-in is required. Run `rolino auth login`.");
147
+ }
148
+ const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : 300;
149
+ const refreshed = {
150
+ ...current,
151
+ accessToken: payload.access_token,
152
+ accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
153
+ refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : current.refreshToken,
154
+ scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : current.scopes
155
+ };
156
+ saveOAuthTokenSet(refreshed, options.env);
157
+ return refreshed;
158
+ })().finally(() => refreshLocks.delete(key));
159
+ refreshLocks.set(key, refresh);
160
+ return refresh;
161
+ }
162
+ function createOAuthAccessTokenProvider(options) {
163
+ const env = options.env ?? process.env;
164
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
165
+ const obtain = async (force) => {
166
+ if (env.ROLINO_TOKEN) return env.ROLINO_TOKEN;
167
+ const token = loadOAuthTokenSet(options.baseUrl, env);
168
+ if (!token) return void 0;
169
+ if (!force && Date.parse(token.accessTokenExpiresAt) - Date.now() > 3e4) {
170
+ return token.accessToken;
171
+ }
172
+ return (await refreshOAuthTokenSet({
173
+ baseUrl: options.baseUrl,
174
+ env,
175
+ fetch: fetchImplementation
176
+ })).accessToken;
177
+ };
178
+ const provider = (() => obtain(false));
179
+ provider.refresh = () => obtain(true);
180
+ return provider;
181
+ }
182
+ function resolveLocalAuthentication(baseUrl, env = process.env) {
183
+ if (env.ROLINO_TOKEN) return { source: "environment", tokenSet: null };
184
+ const tokenSet = loadOAuthTokenSet(baseUrl, env);
185
+ return tokenSet ? { source: "oauth", tokenSet } : { source: "none", tokenSet: null };
108
186
  }
109
187
  // Annotate the CommonJS export names for ESM import in node:
110
188
  0 && (module.exports = {
111
- clearStoredCredential,
112
- credentialStorePath,
113
- loadStoredCredential,
114
- resolveCredential,
115
- saveStoredCredential
189
+ ROLINO_CLI_OAUTH_CLIENT_ID,
190
+ clearOAuthTokenSet,
191
+ createOAuthAccessTokenProvider,
192
+ loadOAuthTokenSet,
193
+ oauthConfiguration,
194
+ oauthTokenStorePath,
195
+ resolveLocalAuthentication,
196
+ saveOAuthTokenSet
116
197
  });
117
198
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\r\n chmodSync,\r\n existsSync,\r\n mkdirSync,\r\n readFileSync,\r\n unlinkSync,\r\n writeFileSync,\r\n} from \"node:fs\";\r\nimport { homedir } from \"node:os\";\r\nimport { dirname, join } from \"node:path\";\r\n\r\nimport { normalizeRolinoBaseUrl } from \"@rolino/sdk\";\r\n\r\nexport type StoredCredential = {\r\n token: string;\r\n createdAt: string;\r\n expiresAt: string;\r\n organization: { id: string; name: string };\r\n};\r\n\r\ntype CredentialStore = {\r\n version: 1;\r\n credentials: Record<string, StoredCredential>;\r\n};\r\n\r\nfunction defaultConfigDirectory(env: Record<string, string | undefined>) {\r\n if (env.ROLINO_CONFIG_DIR) return env.ROLINO_CONFIG_DIR;\r\n if (process.platform === \"win32\" && env.APPDATA) {\r\n return join(env.APPDATA, \"Rolino\");\r\n }\r\n if (process.platform === \"darwin\") {\r\n return join(homedir(), \"Library\", \"Application Support\", \"Rolino\");\r\n }\r\n return join(env.XDG_CONFIG_HOME ?? join(homedir(), \".config\"), \"rolino\");\r\n}\r\n\r\nexport function credentialStorePath(\r\n env: Record<string, string | undefined> = process.env,\r\n) {\r\n return join(defaultConfigDirectory(env), \"credentials.json\");\r\n}\r\n\r\nfunction isStoredCredential(value: unknown): value is StoredCredential {\r\n if (!value || typeof value !== \"object\") return false;\r\n const credential = value as Partial<StoredCredential>;\r\n return typeof credential.token === \"string\"\r\n && credential.token.length > 0\r\n && typeof credential.createdAt === \"string\"\r\n && typeof credential.expiresAt === \"string\"\r\n && Boolean(credential.organization)\r\n && typeof credential.organization?.id === \"string\"\r\n && typeof credential.organization?.name === \"string\";\r\n}\r\n\r\nfunction readStore(env: Record<string, string | undefined>): CredentialStore {\r\n const path = credentialStorePath(env);\r\n if (!existsSync(path)) return { version: 1, credentials: {} };\r\n\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(readFileSync(path, \"utf8\"));\r\n } catch {\r\n throw new TypeError(`Rolino could not read its credential store at ${path}.`);\r\n }\r\n if (!parsed || typeof parsed !== \"object\") {\r\n throw new TypeError(`Rolino's credential store at ${path} is invalid.`);\r\n }\r\n const candidate = parsed as Partial<CredentialStore>;\r\n if (\r\n candidate.version !== 1\r\n || !candidate.credentials\r\n || typeof candidate.credentials !== \"object\"\r\n || !Object.values(candidate.credentials).every(isStoredCredential)\r\n ) {\r\n throw new TypeError(`Rolino's credential store at ${path} is invalid.`);\r\n }\r\n return candidate as CredentialStore;\r\n}\r\n\r\nfunction writeStore(\r\n store: CredentialStore,\r\n env: Record<string, string | undefined>,\r\n) {\r\n const path = credentialStorePath(env);\r\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\r\n writeFileSync(path, `${JSON.stringify(store, null, 2)}\\n`, {\r\n encoding: \"utf8\",\r\n mode: 0o600,\r\n });\r\n chmodSync(path, 0o600);\r\n}\r\n\r\nexport function loadStoredCredential(\r\n baseUrl: string,\r\n env: Record<string, string | undefined> = process.env,\r\n) {\r\n return readStore(env).credentials[normalizeRolinoBaseUrl(baseUrl)] ?? null;\r\n}\r\n\r\nexport function saveStoredCredential(\r\n baseUrl: string,\r\n credential: StoredCredential,\r\n env: Record<string, string | undefined> = process.env,\r\n) {\r\n const store = readStore(env);\r\n store.credentials[normalizeRolinoBaseUrl(baseUrl)] = credential;\r\n writeStore(store, env);\r\n return credentialStorePath(env);\r\n}\r\n\r\nexport function clearStoredCredential(\r\n baseUrl: string,\r\n env: Record<string, string | undefined> = process.env,\r\n) {\r\n const path = credentialStorePath(env);\r\n const store = readStore(env);\r\n const key = normalizeRolinoBaseUrl(baseUrl);\r\n const existed = Boolean(store.credentials[key]);\r\n delete store.credentials[key];\r\n\r\n if (Object.keys(store.credentials).length === 0) {\r\n if (existsSync(path)) unlinkSync(path);\r\n } else {\r\n writeStore(store, env);\r\n }\r\n return existed;\r\n}\r\n\r\nexport function resolveCredential(\r\n baseUrl: string,\r\n env: Record<string, string | undefined> = process.env,\r\n) {\r\n if (env.ROLINO_TOKEN) {\r\n return { source: \"environment\" as const, token: env.ROLINO_TOKEN };\r\n }\r\n const stored = loadStoredCredential(baseUrl, env);\r\n return stored\r\n ? { source: \"stored\" as const, token: stored.token, credential: stored }\r\n : { source: \"none\" as const, token: null };\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAOO;AACP,qBAAwB;AACxB,uBAA8B;AAE9B,iBAAuC;AAcvC,SAAS,uBAAuB,KAAyC;AACvE,MAAI,IAAI,kBAAmB,QAAO,IAAI;AACtC,MAAI,QAAQ,aAAa,WAAW,IAAI,SAAS;AAC/C,eAAO,uBAAK,IAAI,SAAS,QAAQ;AAAA,EACnC;AACA,MAAI,QAAQ,aAAa,UAAU;AACjC,eAAO,2BAAK,wBAAQ,GAAG,WAAW,uBAAuB,QAAQ;AAAA,EACnE;AACA,aAAO,uBAAK,IAAI,uBAAmB,2BAAK,wBAAQ,GAAG,SAAS,GAAG,QAAQ;AACzE;AAEO,SAAS,oBACd,MAA0C,QAAQ,KAClD;AACA,aAAO,uBAAK,uBAAuB,GAAG,GAAG,kBAAkB;AAC7D;AAEA,SAAS,mBAAmB,OAA2C;AACrE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,aAAa;AACnB,SAAO,OAAO,WAAW,UAAU,YAC9B,WAAW,MAAM,SAAS,KAC1B,OAAO,WAAW,cAAc,YAChC,OAAO,WAAW,cAAc,YAChC,QAAQ,WAAW,YAAY,KAC/B,OAAO,WAAW,cAAc,OAAO,YACvC,OAAO,WAAW,cAAc,SAAS;AAChD;AAEA,SAAS,UAAU,KAA0D;AAC3E,QAAM,OAAO,oBAAoB,GAAG;AACpC,MAAI,KAAC,2BAAW,IAAI,EAAG,QAAO,EAAE,SAAS,GAAG,aAAa,CAAC,EAAE;AAE5D,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,UAAM,6BAAa,MAAM,MAAM,CAAC;AAAA,EAChD,QAAQ;AACN,UAAM,IAAI,UAAU,iDAAiD,IAAI,GAAG;AAAA,EAC9E;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,UAAM,IAAI,UAAU,gCAAgC,IAAI,cAAc;AAAA,EACxE;AACA,QAAM,YAAY;AAClB,MACE,UAAU,YAAY,KACnB,CAAC,UAAU,eACX,OAAO,UAAU,gBAAgB,YACjC,CAAC,OAAO,OAAO,UAAU,WAAW,EAAE,MAAM,kBAAkB,GACjE;AACA,UAAM,IAAI,UAAU,gCAAgC,IAAI,cAAc;AAAA,EACxE;AACA,SAAO;AACT;AAEA,SAAS,WACP,OACA,KACA;AACA,QAAM,OAAO,oBAAoB,GAAG;AACpC,oCAAU,0BAAQ,IAAI,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACzD,oCAAc,MAAM,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,GAAM;AAAA,IACzD,UAAU;AAAA,IACV,MAAM;AAAA,EACR,CAAC;AACD,gCAAU,MAAM,GAAK;AACvB;AAEO,SAAS,qBACd,SACA,MAA0C,QAAQ,KAClD;AACA,SAAO,UAAU,GAAG,EAAE,gBAAY,mCAAuB,OAAO,CAAC,KAAK;AACxE;AAEO,SAAS,qBACd,SACA,YACA,MAA0C,QAAQ,KAClD;AACA,QAAM,QAAQ,UAAU,GAAG;AAC3B,QAAM,gBAAY,mCAAuB,OAAO,CAAC,IAAI;AACrD,aAAW,OAAO,GAAG;AACrB,SAAO,oBAAoB,GAAG;AAChC;AAEO,SAAS,sBACd,SACA,MAA0C,QAAQ,KAClD;AACA,QAAM,OAAO,oBAAoB,GAAG;AACpC,QAAM,QAAQ,UAAU,GAAG;AAC3B,QAAM,UAAM,mCAAuB,OAAO;AAC1C,QAAM,UAAU,QAAQ,MAAM,YAAY,GAAG,CAAC;AAC9C,SAAO,MAAM,YAAY,GAAG;AAE5B,MAAI,OAAO,KAAK,MAAM,WAAW,EAAE,WAAW,GAAG;AAC/C,YAAI,2BAAW,IAAI,EAAG,gCAAW,IAAI;AAAA,EACvC,OAAO;AACL,eAAW,OAAO,GAAG;AAAA,EACvB;AACA,SAAO;AACT;AAEO,SAAS,kBACd,SACA,MAA0C,QAAQ,KAClD;AACA,MAAI,IAAI,cAAc;AACpB,WAAO,EAAE,QAAQ,eAAwB,OAAO,IAAI,aAAa;AAAA,EACnE;AACA,QAAM,SAAS,qBAAqB,SAAS,GAAG;AAChD,SAAO,SACH,EAAE,QAAQ,UAAmB,OAAO,OAAO,OAAO,YAAY,OAAO,IACrE,EAAE,QAAQ,QAAiB,OAAO,KAAK;AAC7C;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n chmodSync,\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport { normalizeRolinoBaseUrl } from \"@rolino/sdk\";\n\nexport const ROLINO_CLI_OAUTH_CLIENT_ID = \"rolino-cli\";\n\nexport type OAuthTokenSet = {\n host: string;\n issuer: string;\n clientId: string;\n resource: string;\n accessToken: string;\n accessTokenExpiresAt: string;\n refreshToken: string;\n refreshTokenExpiresAt: string;\n scopes: string[];\n};\n\ntype OAuthTokenStore = {\n version: 2;\n tokens: Record<string, OAuthTokenSet>;\n};\n\ntype RefreshableTokenProvider = (() => Promise<string | undefined>) & {\n refresh: () => Promise<string | undefined>;\n};\n\nconst refreshLocks = new Map<string, Promise<OAuthTokenSet>>();\n\nfunction defaultConfigDirectory(env: Record<string, string | undefined>) {\n if (env.ROLINO_CONFIG_DIR) return env.ROLINO_CONFIG_DIR;\n if (process.platform === \"win32\" && env.APPDATA) return join(env.APPDATA, \"Rolino\");\n if (process.platform === \"darwin\") return join(homedir(), \"Library\", \"Application Support\", \"Rolino\");\n return join(env.XDG_CONFIG_HOME ?? join(homedir(), \".config\"), \"rolino\");\n}\n\nexport function oauthTokenStorePath(\n env: Record<string, string | undefined> = process.env,\n) {\n return join(defaultConfigDirectory(env), \"oauth-tokens.json\");\n}\n\nexport function oauthConfiguration(baseUrl: string) {\n const host = normalizeRolinoBaseUrl(baseUrl);\n return {\n host,\n issuer: `${host}/api/auth`,\n clientId: ROLINO_CLI_OAUTH_CLIENT_ID,\n resource: `${host}/api/v1`,\n };\n}\n\nfunction tokenKey(input: Pick<OAuthTokenSet, \"host\" | \"issuer\" | \"clientId\" | \"resource\">) {\n return JSON.stringify([input.host, input.issuer, input.clientId, input.resource]);\n}\n\nfunction isTokenSet(value: unknown): value is OAuthTokenSet {\n if (!value || typeof value !== \"object\") return false;\n const token = value as Partial<OAuthTokenSet>;\n return [token.host, token.issuer, token.clientId, token.resource, token.accessToken,\n token.accessTokenExpiresAt, token.refreshToken, token.refreshTokenExpiresAt]\n .every((item) => typeof item === \"string\" && item.length > 0)\n && Array.isArray(token.scopes)\n && token.scopes.every((scope) => typeof scope === \"string\");\n}\n\nfunction readStore(env: Record<string, string | undefined>): OAuthTokenStore {\n const path = oauthTokenStorePath(env);\n if (!existsSync(path)) return { version: 2, tokens: {} };\n try {\n const parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<OAuthTokenStore>;\n if (\n parsed.version !== 2\n || !parsed.tokens\n || typeof parsed.tokens !== \"object\"\n || !Object.values(parsed.tokens).every(isTokenSet)\n ) throw new Error(\"invalid\");\n return parsed as OAuthTokenStore;\n } catch {\n throw new TypeError(`Rolino could not read its OAuth token store at ${path}.`);\n }\n}\n\nfunction writeStore(store: OAuthTokenStore, env: Record<string, string | undefined>) {\n const path = oauthTokenStorePath(env);\n const temporary = `${path}.${process.pid}.tmp`;\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n writeFileSync(temporary, `${JSON.stringify(store, null, 2)}\\n`, {\n encoding: \"utf8\",\n mode: 0o600,\n });\n chmodSync(temporary, 0o600);\n renameSync(temporary, path);\n chmodSync(path, 0o600);\n return path;\n}\n\nexport function loadOAuthTokenSet(\n baseUrl: string,\n env: Record<string, string | undefined> = process.env,\n) {\n const configuration = oauthConfiguration(baseUrl);\n return readStore(env).tokens[tokenKey(configuration)] ?? null;\n}\n\nexport function saveOAuthTokenSet(\n tokenSet: OAuthTokenSet,\n env: Record<string, string | undefined> = process.env,\n) {\n const configuration = oauthConfiguration(tokenSet.host);\n if (\n tokenSet.host !== configuration.host\n || tokenSet.issuer !== configuration.issuer\n || tokenSet.clientId !== configuration.clientId\n || tokenSet.resource !== configuration.resource\n ) throw new TypeError(\"The OAuth token set does not match the Rolino host.\");\n const store = readStore(env);\n store.tokens[tokenKey(tokenSet)] = tokenSet;\n const path = writeStore(store, env);\n const retiredInteractiveCredential = join(defaultConfigDirectory(env), \"credentials.json\");\n if (existsSync(retiredInteractiveCredential)) unlinkSync(retiredInteractiveCredential);\n return path;\n}\n\nexport function clearOAuthTokenSet(\n baseUrl: string,\n env: Record<string, string | undefined> = process.env,\n) {\n const path = oauthTokenStorePath(env);\n const store = readStore(env);\n const key = tokenKey(oauthConfiguration(baseUrl));\n const existed = Boolean(store.tokens[key]);\n delete store.tokens[key];\n if (!Object.keys(store.tokens).length) {\n if (existsSync(path)) unlinkSync(path);\n } else {\n writeStore(store, env);\n }\n return existed;\n}\n\nasync function refreshOAuthTokenSet(options: {\n baseUrl: string;\n env: Record<string, string | undefined>;\n fetch: typeof globalThis.fetch;\n}) {\n const current = loadOAuthTokenSet(options.baseUrl, options.env);\n if (!current) throw new TypeError(\"OAuth sign-in is required. Run `rolino auth login`.\");\n const key = tokenKey(current);\n const existing = refreshLocks.get(key);\n if (existing) return existing;\n\n const refresh = (async () => {\n const body = new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: current.refreshToken,\n client_id: current.clientId,\n resource: current.resource,\n });\n const response = await options.fetch(`${current.issuer}/oauth2/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\", accept: \"application/json\" },\n body,\n });\n const payload = await response.json().catch(() => null) as Record<string, unknown> | null;\n if (!response.ok || !payload || typeof payload.access_token !== \"string\") {\n throw new TypeError(\"OAuth sign-in is required. Run `rolino auth login`.\");\n }\n const expiresIn = typeof payload.expires_in === \"number\" ? payload.expires_in : 300;\n const refreshed: OAuthTokenSet = {\n ...current,\n accessToken: payload.access_token,\n accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1_000).toISOString(),\n refreshToken: typeof payload.refresh_token === \"string\" ? payload.refresh_token : current.refreshToken,\n scopes: typeof payload.scope === \"string\" ? payload.scope.split(/\\s+/).filter(Boolean) : current.scopes,\n };\n saveOAuthTokenSet(refreshed, options.env);\n return refreshed;\n })().finally(() => refreshLocks.delete(key));\n refreshLocks.set(key, refresh);\n return refresh;\n}\n\nexport function createOAuthAccessTokenProvider(options: {\n baseUrl: string;\n env?: Record<string, string | undefined>;\n fetch?: typeof globalThis.fetch;\n}): RefreshableTokenProvider {\n const env = options.env ?? process.env;\n const fetchImplementation = options.fetch ?? globalThis.fetch;\n const obtain = async (force: boolean) => {\n if (env.ROLINO_TOKEN) return env.ROLINO_TOKEN;\n const token = loadOAuthTokenSet(options.baseUrl, env);\n if (!token) return undefined;\n if (!force && Date.parse(token.accessTokenExpiresAt) - Date.now() > 30_000) {\n return token.accessToken;\n }\n return (await refreshOAuthTokenSet({\n baseUrl: options.baseUrl,\n env,\n fetch: fetchImplementation,\n })).accessToken;\n };\n const provider = (() => obtain(false)) as RefreshableTokenProvider;\n provider.refresh = () => obtain(true);\n return provider;\n}\n\nexport function resolveLocalAuthentication(\n baseUrl: string,\n env: Record<string, string | undefined> = process.env,\n) {\n if (env.ROLINO_TOKEN) return { source: \"environment\" as const, tokenSet: null };\n const tokenSet = loadOAuthTokenSet(baseUrl, env);\n return tokenSet\n ? { source: \"oauth\" as const, tokenSet }\n : { source: \"none\" as const, tokenSet: null };\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,28 +1,42 @@
1
- type StoredCredential = {
2
- token: string;
3
- createdAt: string;
4
- expiresAt: string;
5
- organization: {
6
- id: string;
7
- name: string;
8
- };
1
+ declare const ROLINO_CLI_OAUTH_CLIENT_ID = "rolino-cli";
2
+ type OAuthTokenSet = {
3
+ host: string;
4
+ issuer: string;
5
+ clientId: string;
6
+ resource: string;
7
+ accessToken: string;
8
+ accessTokenExpiresAt: string;
9
+ refreshToken: string;
10
+ refreshTokenExpiresAt: string;
11
+ scopes: string[];
9
12
  };
10
- declare function credentialStorePath(env?: Record<string, string | undefined>): string;
11
- declare function loadStoredCredential(baseUrl: string, env?: Record<string, string | undefined>): StoredCredential | null;
12
- declare function saveStoredCredential(baseUrl: string, credential: StoredCredential, env?: Record<string, string | undefined>): string;
13
- declare function clearStoredCredential(baseUrl: string, env?: Record<string, string | undefined>): boolean;
14
- declare function resolveCredential(baseUrl: string, env?: Record<string, string | undefined>): {
13
+ type RefreshableTokenProvider = (() => Promise<string | undefined>) & {
14
+ refresh: () => Promise<string | undefined>;
15
+ };
16
+ declare function oauthTokenStorePath(env?: Record<string, string | undefined>): string;
17
+ declare function oauthConfiguration(baseUrl: string): {
18
+ host: string;
19
+ issuer: string;
20
+ clientId: string;
21
+ resource: string;
22
+ };
23
+ declare function loadOAuthTokenSet(baseUrl: string, env?: Record<string, string | undefined>): OAuthTokenSet | null;
24
+ declare function saveOAuthTokenSet(tokenSet: OAuthTokenSet, env?: Record<string, string | undefined>): string;
25
+ declare function clearOAuthTokenSet(baseUrl: string, env?: Record<string, string | undefined>): boolean;
26
+ declare function createOAuthAccessTokenProvider(options: {
27
+ baseUrl: string;
28
+ env?: Record<string, string | undefined>;
29
+ fetch?: typeof globalThis.fetch;
30
+ }): RefreshableTokenProvider;
31
+ declare function resolveLocalAuthentication(baseUrl: string, env?: Record<string, string | undefined>): {
15
32
  source: "environment";
16
- token: string;
17
- credential?: undefined;
33
+ tokenSet: null;
18
34
  } | {
19
- source: "stored";
20
- token: string;
21
- credential: StoredCredential;
35
+ source: "oauth";
36
+ tokenSet: OAuthTokenSet;
22
37
  } | {
23
38
  source: "none";
24
- token: null;
25
- credential?: undefined;
39
+ tokenSet: null;
26
40
  };
27
41
 
28
- export { type StoredCredential, clearStoredCredential, credentialStorePath, loadStoredCredential, resolveCredential, saveStoredCredential };
42
+ export { type OAuthTokenSet, ROLINO_CLI_OAUTH_CLIENT_ID, clearOAuthTokenSet, createOAuthAccessTokenProvider, loadOAuthTokenSet, oauthConfiguration, oauthTokenStorePath, resolveLocalAuthentication, saveOAuthTokenSet };
package/dist/index.d.ts CHANGED
@@ -1,28 +1,42 @@
1
- type StoredCredential = {
2
- token: string;
3
- createdAt: string;
4
- expiresAt: string;
5
- organization: {
6
- id: string;
7
- name: string;
8
- };
1
+ declare const ROLINO_CLI_OAUTH_CLIENT_ID = "rolino-cli";
2
+ type OAuthTokenSet = {
3
+ host: string;
4
+ issuer: string;
5
+ clientId: string;
6
+ resource: string;
7
+ accessToken: string;
8
+ accessTokenExpiresAt: string;
9
+ refreshToken: string;
10
+ refreshTokenExpiresAt: string;
11
+ scopes: string[];
9
12
  };
10
- declare function credentialStorePath(env?: Record<string, string | undefined>): string;
11
- declare function loadStoredCredential(baseUrl: string, env?: Record<string, string | undefined>): StoredCredential | null;
12
- declare function saveStoredCredential(baseUrl: string, credential: StoredCredential, env?: Record<string, string | undefined>): string;
13
- declare function clearStoredCredential(baseUrl: string, env?: Record<string, string | undefined>): boolean;
14
- declare function resolveCredential(baseUrl: string, env?: Record<string, string | undefined>): {
13
+ type RefreshableTokenProvider = (() => Promise<string | undefined>) & {
14
+ refresh: () => Promise<string | undefined>;
15
+ };
16
+ declare function oauthTokenStorePath(env?: Record<string, string | undefined>): string;
17
+ declare function oauthConfiguration(baseUrl: string): {
18
+ host: string;
19
+ issuer: string;
20
+ clientId: string;
21
+ resource: string;
22
+ };
23
+ declare function loadOAuthTokenSet(baseUrl: string, env?: Record<string, string | undefined>): OAuthTokenSet | null;
24
+ declare function saveOAuthTokenSet(tokenSet: OAuthTokenSet, env?: Record<string, string | undefined>): string;
25
+ declare function clearOAuthTokenSet(baseUrl: string, env?: Record<string, string | undefined>): boolean;
26
+ declare function createOAuthAccessTokenProvider(options: {
27
+ baseUrl: string;
28
+ env?: Record<string, string | undefined>;
29
+ fetch?: typeof globalThis.fetch;
30
+ }): RefreshableTokenProvider;
31
+ declare function resolveLocalAuthentication(baseUrl: string, env?: Record<string, string | undefined>): {
15
32
  source: "environment";
16
- token: string;
17
- credential?: undefined;
33
+ tokenSet: null;
18
34
  } | {
19
- source: "stored";
20
- token: string;
21
- credential: StoredCredential;
35
+ source: "oauth";
36
+ tokenSet: OAuthTokenSet;
22
37
  } | {
23
38
  source: "none";
24
- token: null;
25
- credential?: undefined;
39
+ tokenSet: null;
26
40
  };
27
41
 
28
- export { type StoredCredential, clearStoredCredential, credentialStorePath, loadStoredCredential, resolveCredential, saveStoredCredential };
42
+ export { type OAuthTokenSet, ROLINO_CLI_OAUTH_CLIENT_ID, clearOAuthTokenSet, createOAuthAccessTokenProvider, loadOAuthTokenSet, oauthConfiguration, oauthTokenStorePath, resolveLocalAuthentication, saveOAuthTokenSet };
package/dist/index.js CHANGED
@@ -4,92 +4,171 @@ import {
4
4
  existsSync,
5
5
  mkdirSync,
6
6
  readFileSync,
7
+ renameSync,
7
8
  unlinkSync,
8
9
  writeFileSync
9
10
  } from "fs";
10
11
  import { homedir } from "os";
11
12
  import { dirname, join } from "path";
12
13
  import { normalizeRolinoBaseUrl } from "@rolino/sdk";
14
+ var ROLINO_CLI_OAUTH_CLIENT_ID = "rolino-cli";
15
+ var refreshLocks = /* @__PURE__ */ new Map();
13
16
  function defaultConfigDirectory(env) {
14
17
  if (env.ROLINO_CONFIG_DIR) return env.ROLINO_CONFIG_DIR;
15
- if (process.platform === "win32" && env.APPDATA) {
16
- return join(env.APPDATA, "Rolino");
17
- }
18
- if (process.platform === "darwin") {
19
- return join(homedir(), "Library", "Application Support", "Rolino");
20
- }
18
+ if (process.platform === "win32" && env.APPDATA) return join(env.APPDATA, "Rolino");
19
+ if (process.platform === "darwin") return join(homedir(), "Library", "Application Support", "Rolino");
21
20
  return join(env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "rolino");
22
21
  }
23
- function credentialStorePath(env = process.env) {
24
- return join(defaultConfigDirectory(env), "credentials.json");
22
+ function oauthTokenStorePath(env = process.env) {
23
+ return join(defaultConfigDirectory(env), "oauth-tokens.json");
24
+ }
25
+ function oauthConfiguration(baseUrl) {
26
+ const host = normalizeRolinoBaseUrl(baseUrl);
27
+ return {
28
+ host,
29
+ issuer: `${host}/api/auth`,
30
+ clientId: ROLINO_CLI_OAUTH_CLIENT_ID,
31
+ resource: `${host}/api/v1`
32
+ };
25
33
  }
26
- function isStoredCredential(value) {
34
+ function tokenKey(input) {
35
+ return JSON.stringify([input.host, input.issuer, input.clientId, input.resource]);
36
+ }
37
+ function isTokenSet(value) {
27
38
  if (!value || typeof value !== "object") return false;
28
- const credential = value;
29
- return typeof credential.token === "string" && credential.token.length > 0 && typeof credential.createdAt === "string" && typeof credential.expiresAt === "string" && Boolean(credential.organization) && typeof credential.organization?.id === "string" && typeof credential.organization?.name === "string";
39
+ const token = value;
40
+ return [
41
+ token.host,
42
+ token.issuer,
43
+ token.clientId,
44
+ token.resource,
45
+ token.accessToken,
46
+ token.accessTokenExpiresAt,
47
+ token.refreshToken,
48
+ token.refreshTokenExpiresAt
49
+ ].every((item) => typeof item === "string" && item.length > 0) && Array.isArray(token.scopes) && token.scopes.every((scope) => typeof scope === "string");
30
50
  }
31
51
  function readStore(env) {
32
- const path = credentialStorePath(env);
33
- if (!existsSync(path)) return { version: 1, credentials: {} };
34
- let parsed;
52
+ const path = oauthTokenStorePath(env);
53
+ if (!existsSync(path)) return { version: 2, tokens: {} };
35
54
  try {
36
- parsed = JSON.parse(readFileSync(path, "utf8"));
55
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
56
+ if (parsed.version !== 2 || !parsed.tokens || typeof parsed.tokens !== "object" || !Object.values(parsed.tokens).every(isTokenSet)) throw new Error("invalid");
57
+ return parsed;
37
58
  } catch {
38
- throw new TypeError(`Rolino could not read its credential store at ${path}.`);
39
- }
40
- if (!parsed || typeof parsed !== "object") {
41
- throw new TypeError(`Rolino's credential store at ${path} is invalid.`);
59
+ throw new TypeError(`Rolino could not read its OAuth token store at ${path}.`);
42
60
  }
43
- const candidate = parsed;
44
- if (candidate.version !== 1 || !candidate.credentials || typeof candidate.credentials !== "object" || !Object.values(candidate.credentials).every(isStoredCredential)) {
45
- throw new TypeError(`Rolino's credential store at ${path} is invalid.`);
46
- }
47
- return candidate;
48
61
  }
49
62
  function writeStore(store, env) {
50
- const path = credentialStorePath(env);
63
+ const path = oauthTokenStorePath(env);
64
+ const temporary = `${path}.${process.pid}.tmp`;
51
65
  mkdirSync(dirname(path), { recursive: true, mode: 448 });
52
- writeFileSync(path, `${JSON.stringify(store, null, 2)}
66
+ writeFileSync(temporary, `${JSON.stringify(store, null, 2)}
53
67
  `, {
54
68
  encoding: "utf8",
55
69
  mode: 384
56
70
  });
71
+ chmodSync(temporary, 384);
72
+ renameSync(temporary, path);
57
73
  chmodSync(path, 384);
74
+ return path;
58
75
  }
59
- function loadStoredCredential(baseUrl, env = process.env) {
60
- return readStore(env).credentials[normalizeRolinoBaseUrl(baseUrl)] ?? null;
76
+ function loadOAuthTokenSet(baseUrl, env = process.env) {
77
+ const configuration = oauthConfiguration(baseUrl);
78
+ return readStore(env).tokens[tokenKey(configuration)] ?? null;
61
79
  }
62
- function saveStoredCredential(baseUrl, credential, env = process.env) {
80
+ function saveOAuthTokenSet(tokenSet, env = process.env) {
81
+ const configuration = oauthConfiguration(tokenSet.host);
82
+ if (tokenSet.host !== configuration.host || tokenSet.issuer !== configuration.issuer || tokenSet.clientId !== configuration.clientId || tokenSet.resource !== configuration.resource) throw new TypeError("The OAuth token set does not match the Rolino host.");
63
83
  const store = readStore(env);
64
- store.credentials[normalizeRolinoBaseUrl(baseUrl)] = credential;
65
- writeStore(store, env);
66
- return credentialStorePath(env);
84
+ store.tokens[tokenKey(tokenSet)] = tokenSet;
85
+ const path = writeStore(store, env);
86
+ const retiredInteractiveCredential = join(defaultConfigDirectory(env), "credentials.json");
87
+ if (existsSync(retiredInteractiveCredential)) unlinkSync(retiredInteractiveCredential);
88
+ return path;
67
89
  }
68
- function clearStoredCredential(baseUrl, env = process.env) {
69
- const path = credentialStorePath(env);
90
+ function clearOAuthTokenSet(baseUrl, env = process.env) {
91
+ const path = oauthTokenStorePath(env);
70
92
  const store = readStore(env);
71
- const key = normalizeRolinoBaseUrl(baseUrl);
72
- const existed = Boolean(store.credentials[key]);
73
- delete store.credentials[key];
74
- if (Object.keys(store.credentials).length === 0) {
93
+ const key = tokenKey(oauthConfiguration(baseUrl));
94
+ const existed = Boolean(store.tokens[key]);
95
+ delete store.tokens[key];
96
+ if (!Object.keys(store.tokens).length) {
75
97
  if (existsSync(path)) unlinkSync(path);
76
98
  } else {
77
99
  writeStore(store, env);
78
100
  }
79
101
  return existed;
80
102
  }
81
- function resolveCredential(baseUrl, env = process.env) {
82
- if (env.ROLINO_TOKEN) {
83
- return { source: "environment", token: env.ROLINO_TOKEN };
84
- }
85
- const stored = loadStoredCredential(baseUrl, env);
86
- return stored ? { source: "stored", token: stored.token, credential: stored } : { source: "none", token: null };
103
+ async function refreshOAuthTokenSet(options) {
104
+ const current = loadOAuthTokenSet(options.baseUrl, options.env);
105
+ if (!current) throw new TypeError("OAuth sign-in is required. Run `rolino auth login`.");
106
+ const key = tokenKey(current);
107
+ const existing = refreshLocks.get(key);
108
+ if (existing) return existing;
109
+ const refresh = (async () => {
110
+ const body = new URLSearchParams({
111
+ grant_type: "refresh_token",
112
+ refresh_token: current.refreshToken,
113
+ client_id: current.clientId,
114
+ resource: current.resource
115
+ });
116
+ const response = await options.fetch(`${current.issuer}/oauth2/token`, {
117
+ method: "POST",
118
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
119
+ body
120
+ });
121
+ const payload = await response.json().catch(() => null);
122
+ if (!response.ok || !payload || typeof payload.access_token !== "string") {
123
+ throw new TypeError("OAuth sign-in is required. Run `rolino auth login`.");
124
+ }
125
+ const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : 300;
126
+ const refreshed = {
127
+ ...current,
128
+ accessToken: payload.access_token,
129
+ accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
130
+ refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : current.refreshToken,
131
+ scopes: typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : current.scopes
132
+ };
133
+ saveOAuthTokenSet(refreshed, options.env);
134
+ return refreshed;
135
+ })().finally(() => refreshLocks.delete(key));
136
+ refreshLocks.set(key, refresh);
137
+ return refresh;
138
+ }
139
+ function createOAuthAccessTokenProvider(options) {
140
+ const env = options.env ?? process.env;
141
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
142
+ const obtain = async (force) => {
143
+ if (env.ROLINO_TOKEN) return env.ROLINO_TOKEN;
144
+ const token = loadOAuthTokenSet(options.baseUrl, env);
145
+ if (!token) return void 0;
146
+ if (!force && Date.parse(token.accessTokenExpiresAt) - Date.now() > 3e4) {
147
+ return token.accessToken;
148
+ }
149
+ return (await refreshOAuthTokenSet({
150
+ baseUrl: options.baseUrl,
151
+ env,
152
+ fetch: fetchImplementation
153
+ })).accessToken;
154
+ };
155
+ const provider = (() => obtain(false));
156
+ provider.refresh = () => obtain(true);
157
+ return provider;
158
+ }
159
+ function resolveLocalAuthentication(baseUrl, env = process.env) {
160
+ if (env.ROLINO_TOKEN) return { source: "environment", tokenSet: null };
161
+ const tokenSet = loadOAuthTokenSet(baseUrl, env);
162
+ return tokenSet ? { source: "oauth", tokenSet } : { source: "none", tokenSet: null };
87
163
  }
88
164
  export {
89
- clearStoredCredential,
90
- credentialStorePath,
91
- loadStoredCredential,
92
- resolveCredential,
93
- saveStoredCredential
165
+ ROLINO_CLI_OAUTH_CLIENT_ID,
166
+ clearOAuthTokenSet,
167
+ createOAuthAccessTokenProvider,
168
+ loadOAuthTokenSet,
169
+ oauthConfiguration,
170
+ oauthTokenStorePath,
171
+ resolveLocalAuthentication,
172
+ saveOAuthTokenSet
94
173
  };
95
174
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\r\n chmodSync,\r\n existsSync,\r\n mkdirSync,\r\n readFileSync,\r\n unlinkSync,\r\n writeFileSync,\r\n} from \"node:fs\";\r\nimport { homedir } from \"node:os\";\r\nimport { dirname, join } from \"node:path\";\r\n\r\nimport { normalizeRolinoBaseUrl } from \"@rolino/sdk\";\r\n\r\nexport type StoredCredential = {\r\n token: string;\r\n createdAt: string;\r\n expiresAt: string;\r\n organization: { id: string; name: string };\r\n};\r\n\r\ntype CredentialStore = {\r\n version: 1;\r\n credentials: Record<string, StoredCredential>;\r\n};\r\n\r\nfunction defaultConfigDirectory(env: Record<string, string | undefined>) {\r\n if (env.ROLINO_CONFIG_DIR) return env.ROLINO_CONFIG_DIR;\r\n if (process.platform === \"win32\" && env.APPDATA) {\r\n return join(env.APPDATA, \"Rolino\");\r\n }\r\n if (process.platform === \"darwin\") {\r\n return join(homedir(), \"Library\", \"Application Support\", \"Rolino\");\r\n }\r\n return join(env.XDG_CONFIG_HOME ?? join(homedir(), \".config\"), \"rolino\");\r\n}\r\n\r\nexport function credentialStorePath(\r\n env: Record<string, string | undefined> = process.env,\r\n) {\r\n return join(defaultConfigDirectory(env), \"credentials.json\");\r\n}\r\n\r\nfunction isStoredCredential(value: unknown): value is StoredCredential {\r\n if (!value || typeof value !== \"object\") return false;\r\n const credential = value as Partial<StoredCredential>;\r\n return typeof credential.token === \"string\"\r\n && credential.token.length > 0\r\n && typeof credential.createdAt === \"string\"\r\n && typeof credential.expiresAt === \"string\"\r\n && Boolean(credential.organization)\r\n && typeof credential.organization?.id === \"string\"\r\n && typeof credential.organization?.name === \"string\";\r\n}\r\n\r\nfunction readStore(env: Record<string, string | undefined>): CredentialStore {\r\n const path = credentialStorePath(env);\r\n if (!existsSync(path)) return { version: 1, credentials: {} };\r\n\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(readFileSync(path, \"utf8\"));\r\n } catch {\r\n throw new TypeError(`Rolino could not read its credential store at ${path}.`);\r\n }\r\n if (!parsed || typeof parsed !== \"object\") {\r\n throw new TypeError(`Rolino's credential store at ${path} is invalid.`);\r\n }\r\n const candidate = parsed as Partial<CredentialStore>;\r\n if (\r\n candidate.version !== 1\r\n || !candidate.credentials\r\n || typeof candidate.credentials !== \"object\"\r\n || !Object.values(candidate.credentials).every(isStoredCredential)\r\n ) {\r\n throw new TypeError(`Rolino's credential store at ${path} is invalid.`);\r\n }\r\n return candidate as CredentialStore;\r\n}\r\n\r\nfunction writeStore(\r\n store: CredentialStore,\r\n env: Record<string, string | undefined>,\r\n) {\r\n const path = credentialStorePath(env);\r\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\r\n writeFileSync(path, `${JSON.stringify(store, null, 2)}\\n`, {\r\n encoding: \"utf8\",\r\n mode: 0o600,\r\n });\r\n chmodSync(path, 0o600);\r\n}\r\n\r\nexport function loadStoredCredential(\r\n baseUrl: string,\r\n env: Record<string, string | undefined> = process.env,\r\n) {\r\n return readStore(env).credentials[normalizeRolinoBaseUrl(baseUrl)] ?? null;\r\n}\r\n\r\nexport function saveStoredCredential(\r\n baseUrl: string,\r\n credential: StoredCredential,\r\n env: Record<string, string | undefined> = process.env,\r\n) {\r\n const store = readStore(env);\r\n store.credentials[normalizeRolinoBaseUrl(baseUrl)] = credential;\r\n writeStore(store, env);\r\n return credentialStorePath(env);\r\n}\r\n\r\nexport function clearStoredCredential(\r\n baseUrl: string,\r\n env: Record<string, string | undefined> = process.env,\r\n) {\r\n const path = credentialStorePath(env);\r\n const store = readStore(env);\r\n const key = normalizeRolinoBaseUrl(baseUrl);\r\n const existed = Boolean(store.credentials[key]);\r\n delete store.credentials[key];\r\n\r\n if (Object.keys(store.credentials).length === 0) {\r\n if (existsSync(path)) unlinkSync(path);\r\n } else {\r\n writeStore(store, env);\r\n }\r\n return existed;\r\n}\r\n\r\nexport function resolveCredential(\r\n baseUrl: string,\r\n env: Record<string, string | undefined> = process.env,\r\n) {\r\n if (env.ROLINO_TOKEN) {\r\n return { source: \"environment\" as const, token: env.ROLINO_TOKEN };\r\n }\r\n const stored = loadStoredCredential(baseUrl, env);\r\n return stored\r\n ? { source: \"stored\" as const, token: stored.token, credential: stored }\r\n : { source: \"none\" as const, token: null };\r\n}\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n chmodSync,\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport { normalizeRolinoBaseUrl } from \"@rolino/sdk\";\n\nexport const ROLINO_CLI_OAUTH_CLIENT_ID = \"rolino-cli\";\n\nexport type OAuthTokenSet = {\n host: string;\n issuer: string;\n clientId: string;\n resource: string;\n accessToken: string;\n accessTokenExpiresAt: string;\n refreshToken: string;\n refreshTokenExpiresAt: string;\n scopes: string[];\n};\n\ntype OAuthTokenStore = {\n version: 2;\n tokens: Record<string, OAuthTokenSet>;\n};\n\ntype RefreshableTokenProvider = (() => Promise<string | undefined>) & {\n refresh: () => Promise<string | undefined>;\n};\n\nconst refreshLocks = new Map<string, Promise<OAuthTokenSet>>();\n\nfunction defaultConfigDirectory(env: Record<string, string | undefined>) {\n if (env.ROLINO_CONFIG_DIR) return env.ROLINO_CONFIG_DIR;\n if (process.platform === \"win32\" && env.APPDATA) return join(env.APPDATA, \"Rolino\");\n if (process.platform === \"darwin\") return join(homedir(), \"Library\", \"Application Support\", \"Rolino\");\n return join(env.XDG_CONFIG_HOME ?? join(homedir(), \".config\"), \"rolino\");\n}\n\nexport function oauthTokenStorePath(\n env: Record<string, string | undefined> = process.env,\n) {\n return join(defaultConfigDirectory(env), \"oauth-tokens.json\");\n}\n\nexport function oauthConfiguration(baseUrl: string) {\n const host = normalizeRolinoBaseUrl(baseUrl);\n return {\n host,\n issuer: `${host}/api/auth`,\n clientId: ROLINO_CLI_OAUTH_CLIENT_ID,\n resource: `${host}/api/v1`,\n };\n}\n\nfunction tokenKey(input: Pick<OAuthTokenSet, \"host\" | \"issuer\" | \"clientId\" | \"resource\">) {\n return JSON.stringify([input.host, input.issuer, input.clientId, input.resource]);\n}\n\nfunction isTokenSet(value: unknown): value is OAuthTokenSet {\n if (!value || typeof value !== \"object\") return false;\n const token = value as Partial<OAuthTokenSet>;\n return [token.host, token.issuer, token.clientId, token.resource, token.accessToken,\n token.accessTokenExpiresAt, token.refreshToken, token.refreshTokenExpiresAt]\n .every((item) => typeof item === \"string\" && item.length > 0)\n && Array.isArray(token.scopes)\n && token.scopes.every((scope) => typeof scope === \"string\");\n}\n\nfunction readStore(env: Record<string, string | undefined>): OAuthTokenStore {\n const path = oauthTokenStorePath(env);\n if (!existsSync(path)) return { version: 2, tokens: {} };\n try {\n const parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<OAuthTokenStore>;\n if (\n parsed.version !== 2\n || !parsed.tokens\n || typeof parsed.tokens !== \"object\"\n || !Object.values(parsed.tokens).every(isTokenSet)\n ) throw new Error(\"invalid\");\n return parsed as OAuthTokenStore;\n } catch {\n throw new TypeError(`Rolino could not read its OAuth token store at ${path}.`);\n }\n}\n\nfunction writeStore(store: OAuthTokenStore, env: Record<string, string | undefined>) {\n const path = oauthTokenStorePath(env);\n const temporary = `${path}.${process.pid}.tmp`;\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n writeFileSync(temporary, `${JSON.stringify(store, null, 2)}\\n`, {\n encoding: \"utf8\",\n mode: 0o600,\n });\n chmodSync(temporary, 0o600);\n renameSync(temporary, path);\n chmodSync(path, 0o600);\n return path;\n}\n\nexport function loadOAuthTokenSet(\n baseUrl: string,\n env: Record<string, string | undefined> = process.env,\n) {\n const configuration = oauthConfiguration(baseUrl);\n return readStore(env).tokens[tokenKey(configuration)] ?? null;\n}\n\nexport function saveOAuthTokenSet(\n tokenSet: OAuthTokenSet,\n env: Record<string, string | undefined> = process.env,\n) {\n const configuration = oauthConfiguration(tokenSet.host);\n if (\n tokenSet.host !== configuration.host\n || tokenSet.issuer !== configuration.issuer\n || tokenSet.clientId !== configuration.clientId\n || tokenSet.resource !== configuration.resource\n ) throw new TypeError(\"The OAuth token set does not match the Rolino host.\");\n const store = readStore(env);\n store.tokens[tokenKey(tokenSet)] = tokenSet;\n const path = writeStore(store, env);\n const retiredInteractiveCredential = join(defaultConfigDirectory(env), \"credentials.json\");\n if (existsSync(retiredInteractiveCredential)) unlinkSync(retiredInteractiveCredential);\n return path;\n}\n\nexport function clearOAuthTokenSet(\n baseUrl: string,\n env: Record<string, string | undefined> = process.env,\n) {\n const path = oauthTokenStorePath(env);\n const store = readStore(env);\n const key = tokenKey(oauthConfiguration(baseUrl));\n const existed = Boolean(store.tokens[key]);\n delete store.tokens[key];\n if (!Object.keys(store.tokens).length) {\n if (existsSync(path)) unlinkSync(path);\n } else {\n writeStore(store, env);\n }\n return existed;\n}\n\nasync function refreshOAuthTokenSet(options: {\n baseUrl: string;\n env: Record<string, string | undefined>;\n fetch: typeof globalThis.fetch;\n}) {\n const current = loadOAuthTokenSet(options.baseUrl, options.env);\n if (!current) throw new TypeError(\"OAuth sign-in is required. Run `rolino auth login`.\");\n const key = tokenKey(current);\n const existing = refreshLocks.get(key);\n if (existing) return existing;\n\n const refresh = (async () => {\n const body = new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: current.refreshToken,\n client_id: current.clientId,\n resource: current.resource,\n });\n const response = await options.fetch(`${current.issuer}/oauth2/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\", accept: \"application/json\" },\n body,\n });\n const payload = await response.json().catch(() => null) as Record<string, unknown> | null;\n if (!response.ok || !payload || typeof payload.access_token !== \"string\") {\n throw new TypeError(\"OAuth sign-in is required. Run `rolino auth login`.\");\n }\n const expiresIn = typeof payload.expires_in === \"number\" ? payload.expires_in : 300;\n const refreshed: OAuthTokenSet = {\n ...current,\n accessToken: payload.access_token,\n accessTokenExpiresAt: new Date(Date.now() + expiresIn * 1_000).toISOString(),\n refreshToken: typeof payload.refresh_token === \"string\" ? payload.refresh_token : current.refreshToken,\n scopes: typeof payload.scope === \"string\" ? payload.scope.split(/\\s+/).filter(Boolean) : current.scopes,\n };\n saveOAuthTokenSet(refreshed, options.env);\n return refreshed;\n })().finally(() => refreshLocks.delete(key));\n refreshLocks.set(key, refresh);\n return refresh;\n}\n\nexport function createOAuthAccessTokenProvider(options: {\n baseUrl: string;\n env?: Record<string, string | undefined>;\n fetch?: typeof globalThis.fetch;\n}): RefreshableTokenProvider {\n const env = options.env ?? process.env;\n const fetchImplementation = options.fetch ?? globalThis.fetch;\n const obtain = async (force: boolean) => {\n if (env.ROLINO_TOKEN) return env.ROLINO_TOKEN;\n const token = loadOAuthTokenSet(options.baseUrl, env);\n if (!token) return undefined;\n if (!force && Date.parse(token.accessTokenExpiresAt) - Date.now() > 30_000) {\n return token.accessToken;\n }\n return (await refreshOAuthTokenSet({\n baseUrl: options.baseUrl,\n env,\n fetch: fetchImplementation,\n })).accessToken;\n };\n const provider = (() => obtain(false)) as RefreshableTokenProvider;\n provider.refresh = () => obtain(true);\n return provider;\n}\n\nexport function resolveLocalAuthentication(\n baseUrl: string,\n env: Record<string, string | undefined> = process.env,\n) {\n if (env.ROLINO_TOKEN) return { source: \"environment\" as const, tokenSet: null };\n const tokenSet = loadOAuthTokenSet(baseUrl, env);\n return tokenSet\n ? { source: \"oauth\" as const, tokenSet }\n : { source: \"none\" as const, tokenSet: null };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rolino/local-auth",
3
- "version": "0.5.0-beta.0",
3
+ "version": "0.6.0",
4
4
  "description": "Host-scoped local credential storage shared by Rolino's Node.js adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,7 +54,7 @@
54
54
  "typecheck": "tsc --noEmit"
55
55
  },
56
56
  "dependencies": {
57
- "@rolino/sdk": "0.5.0-beta.0"
57
+ "@rolino/sdk": "0.6.0"
58
58
  },
59
59
  "engines": {
60
60
  "node": ">=20.19.0"