@losthex/scribe 0.1.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/dist/pkce.js ADDED
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuthError = exports.PROVIDER = exports.CLIENT_ID = void 0;
4
+ exports.createVerifier = createVerifier;
5
+ exports.challengeFor = challengeFor;
6
+ exports.createState = createState;
7
+ exports.authorizeUrl = authorizeUrl;
8
+ exports.parseCallback = parseCallback;
9
+ exports.exchangeCode = exchangeCode;
10
+ exports.refreshTokens = refreshTokens;
11
+ exports.readAccessToken = readAccessToken;
12
+ exports.needsRefresh = needsRefresh;
13
+ /**
14
+ * Authorization code + PKCE against the Scribe issuer (INFRA-ITD-004,
15
+ * SPEC-008 "Fallback & Testing") — the protocol half, with no IO except
16
+ * the two token calls, both of which take an injectable `fetch`.
17
+ *
18
+ * PKCE and not a client secret because this is a public client: the CLI
19
+ * ships on engineers' laptops, so anything embedded in it is public. The
20
+ * verifier never leaves the process, the challenge is what travels, and
21
+ * the loopback redirect (which the issuer's allowlist already permits, see
22
+ * packages/core/src/auth/redirect.ts) keeps the code off the network entirely.
23
+ */
24
+ const node_crypto_1 = require("node:crypto");
25
+ /** Identifies the CLI to the issuer; public by construction. */
26
+ exports.CLIENT_ID = 'scribe-cli';
27
+ /** The only provider the issuer has (INFRA-ITD-004). */
28
+ exports.PROVIDER = 'google';
29
+ const b64url = (buffer) => buffer.toString('base64url');
30
+ /** 32 random bytes, base64url — RFC 7636 §4.1 length rules satisfied. */
31
+ function createVerifier() {
32
+ return b64url((0, node_crypto_1.randomBytes)(32));
33
+ }
34
+ /** S256: the challenge is the SHA-256 of the verifier, base64url. */
35
+ function challengeFor(verifier) {
36
+ return b64url((0, node_crypto_1.createHash)('sha256').update(verifier).digest());
37
+ }
38
+ function createState() {
39
+ return b64url((0, node_crypto_1.randomBytes)(16));
40
+ }
41
+ function authorizeUrl({ issuer, redirectUri, challenge, state, clientId = exports.CLIENT_ID, provider = exports.PROVIDER, }) {
42
+ const url = new URL(`${issuer.replace(/\/+$/, '')}/authorize`);
43
+ url.searchParams.set('client_id', clientId);
44
+ url.searchParams.set('redirect_uri', redirectUri);
45
+ url.searchParams.set('response_type', 'code');
46
+ url.searchParams.set('state', state);
47
+ url.searchParams.set('provider', provider);
48
+ url.searchParams.set('code_challenge', challenge);
49
+ url.searchParams.set('code_challenge_method', 'S256');
50
+ return url.toString();
51
+ }
52
+ /** Reads the query off the loopback request the browser lands on. */
53
+ function parseCallback(requestUrl) {
54
+ const url = new URL(requestUrl, 'http://127.0.0.1');
55
+ const error = url.searchParams.get('error');
56
+ const description = url.searchParams.get('error_description');
57
+ return {
58
+ code: url.searchParams.get('code') ?? undefined,
59
+ state: url.searchParams.get('state') ?? undefined,
60
+ error: error ? (description ? `${error}: ${description}` : error) : undefined,
61
+ };
62
+ }
63
+ class OAuthError extends Error {
64
+ status;
65
+ constructor(message, status = 0) {
66
+ super(message);
67
+ this.name = 'OAuthError';
68
+ this.status = status;
69
+ }
70
+ }
71
+ exports.OAuthError = OAuthError;
72
+ async function tokenCall(issuer, body, fetchImpl, now) {
73
+ const response = await fetchImpl(`${issuer.replace(/\/+$/, '')}/token`, {
74
+ method: 'POST',
75
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
76
+ body: new URLSearchParams(body).toString(),
77
+ });
78
+ const text = await response.text();
79
+ let payload = {};
80
+ try {
81
+ payload = JSON.parse(text);
82
+ }
83
+ catch {
84
+ // Leave payload empty; the status and raw text carry the message.
85
+ }
86
+ if (!response.ok) {
87
+ const detail = typeof payload.error === 'string' ? payload.error : text.slice(0, 200);
88
+ throw new OAuthError(`issuer rejected the token request (${response.status}): ${detail}`, response.status);
89
+ }
90
+ const access = payload.access_token;
91
+ const refresh = payload.refresh_token;
92
+ if (typeof access !== 'string' || typeof refresh !== 'string') {
93
+ throw new OAuthError('issuer returned no access/refresh token pair');
94
+ }
95
+ const expiresIn = typeof payload.expires_in === 'number' ? payload.expires_in : null;
96
+ return {
97
+ access,
98
+ refresh,
99
+ expiresAt: expiresIn === null ? null : now() + expiresIn * 1000,
100
+ };
101
+ }
102
+ function exchangeCode({ issuer, code, redirectUri, verifier, clientId = exports.CLIENT_ID, fetchImpl = fetch, now = Date.now, }) {
103
+ return tokenCall(issuer, {
104
+ grant_type: 'authorization_code',
105
+ code,
106
+ redirect_uri: redirectUri,
107
+ client_id: clientId,
108
+ code_verifier: verifier,
109
+ }, fetchImpl, now);
110
+ }
111
+ function refreshTokens({ issuer, refresh, clientId = exports.CLIENT_ID, fetchImpl = fetch, now = Date.now, }) {
112
+ return tokenCall(issuer, { grant_type: 'refresh_token', refresh_token: refresh, client_id: clientId }, fetchImpl, now);
113
+ }
114
+ function readAccessToken(token) {
115
+ const parts = token.split('.');
116
+ if (parts.length !== 3)
117
+ return { email: null, expiresAt: null };
118
+ try {
119
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
120
+ const email = payload.properties?.email;
121
+ return {
122
+ email: typeof email === 'string' ? email : null,
123
+ expiresAt: typeof payload.exp === 'number' ? payload.exp * 1000 : null,
124
+ };
125
+ }
126
+ catch {
127
+ return { email: null, expiresAt: null };
128
+ }
129
+ }
130
+ /** True when the access token is gone, expired, or about to be. */
131
+ function needsRefresh(tokens, nowMs, skewSeconds = 60) {
132
+ const expiresAt = tokens.expiresAt ?? readAccessToken(tokens.access).expiresAt;
133
+ if (expiresAt === null)
134
+ return false;
135
+ return expiresAt - skewSeconds * 1000 <= nowMs;
136
+ }
137
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"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"}
package/dist/queue.js ADDED
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NETWORK_VERDICT = exports.MAX_COMMITS_PER_BATCH = exports.MAX_ENTRY_AGE_DAYS = exports.BACKOFF_MS = void 0;
4
+ exports.dayOf = dayOf;
5
+ exports.newEntry = newEntry;
6
+ exports.backoffMs = backoffMs;
7
+ exports.isExpired = isExpired;
8
+ exports.isDue = isDue;
9
+ exports.batches = batches;
10
+ exports.classifyStatus = classifyStatus;
11
+ exports.applyFailure = applyFailure;
12
+ exports.mergeQueue = mergeQueue;
13
+ exports.readQueue = readQueue;
14
+ exports.appendEntry = appendEntry;
15
+ exports.writeQueue = writeQueue;
16
+ exports.appendOutcome = appendOutcome;
17
+ exports.readOutcomes = readOutcomes;
18
+ /**
19
+ * The offline queue: one JSONL file of captured commits, and the retry
20
+ * rules that get them to `/amend` eventually.
21
+ *
22
+ * Capture stays local and offline-safe (SPEC-002): a commit on a plane
23
+ * appends a line and returns, and an unsent queue simply carries over to
24
+ * the next flush. Only flushing touches the network. The file is JSONL
25
+ * rather than JSON because appending one line is atomic enough for two
26
+ * commits landing in the same second, while rewriting a whole array is not.
27
+ *
28
+ * Everything that decides *when* and *what* to send is a pure function
29
+ * here; the IO is the four thin readers/writers at the bottom.
30
+ */
31
+ const node_fs_1 = require("node:fs");
32
+ const node_path_1 = require("node:path");
33
+ const node_crypto_1 = require("node:crypto");
34
+ /**
35
+ * The report day is the *local* date of the commit: `git log %aI` carries
36
+ * the author's own offset, so the first ten characters are already the day
37
+ * the engineer would say they did the work.
38
+ */
39
+ function dayOf(isoWithOffset) {
40
+ const match = isoWithOffset.match(/^(\d{4}-\d{2}-\d{2})/);
41
+ if (match)
42
+ return match[1];
43
+ return new Date(isoWithOffset).toISOString().slice(0, 10);
44
+ }
45
+ function newEntry(input) {
46
+ const now = input.now ?? new Date();
47
+ return {
48
+ id: (0, node_crypto_1.randomUUID)(),
49
+ day: dayOf(input.commit.at),
50
+ project: input.project,
51
+ commit: input.commit,
52
+ queuedAt: now.toISOString(),
53
+ attempts: 0,
54
+ nextAttemptAt: null,
55
+ };
56
+ }
57
+ /** Backoff schedule: minutes at first, a day at worst. */
58
+ exports.BACKOFF_MS = [
59
+ 60_000, // 1 minute
60
+ 5 * 60_000,
61
+ 15 * 60_000,
62
+ 60 * 60_000,
63
+ 6 * 60 * 60_000,
64
+ 24 * 60 * 60_000,
65
+ ];
66
+ function backoffMs(attempts) {
67
+ if (attempts <= 0)
68
+ return 0;
69
+ return exports.BACKOFF_MS[Math.min(attempts, exports.BACKOFF_MS.length) - 1];
70
+ }
71
+ /**
72
+ * How old an entry may get before sending it is hopeless — the endpoint
73
+ * refuses a date past its backfill window, so retrying forever would only
74
+ * hide the queue behind permanent 400s.
75
+ */
76
+ exports.MAX_ENTRY_AGE_DAYS = 30;
77
+ function isExpired(entry, now) {
78
+ const day = new Date(`${entry.day}T00:00:00Z`).getTime();
79
+ return (now.getTime() - day) / 86_400_000 > exports.MAX_ENTRY_AGE_DAYS;
80
+ }
81
+ function isDue(entry, now) {
82
+ return entry.nextAttemptAt === null || entry.nextAttemptAt <= now.getTime();
83
+ }
84
+ /** How many commits ride in one POST — the endpoint's own cap. */
85
+ exports.MAX_COMMITS_PER_BATCH = 100;
86
+ /**
87
+ * Groups due entries into one POST per (day, project): the endpoint writes
88
+ * one amendment row per call, and one row per engagement-day is what the
89
+ * evening draft wants to merge.
90
+ */
91
+ function batches(entries, maxPerBatch = exports.MAX_COMMITS_PER_BATCH) {
92
+ const groups = new Map();
93
+ for (const entry of entries) {
94
+ const key = `${entry.day}\u0000${entry.project}`;
95
+ const group = groups.get(key);
96
+ if (group)
97
+ group.push(entry);
98
+ else
99
+ groups.set(key, [entry]);
100
+ }
101
+ return [...groups.values()].flatMap((group) => {
102
+ const chunks = [];
103
+ for (let i = 0; i < group.length; i += maxPerBatch) {
104
+ chunks.push(group.slice(i, i + maxPerBatch));
105
+ }
106
+ return chunks;
107
+ });
108
+ }
109
+ /**
110
+ * What an HTTP status means for the queue. 401 is its own verdict because
111
+ * it is the one failure a refresh can fix without the engineer; 4xx
112
+ * otherwise means the payload will never be accepted, and keeping it in the
113
+ * queue would only bury the working entries behind it.
114
+ */
115
+ function classifyStatus(status) {
116
+ if (status >= 200 && status < 300)
117
+ return 'sent';
118
+ if (status === 401)
119
+ return 'reauth';
120
+ if (status === 408 || status === 429)
121
+ return 'retry';
122
+ if (status >= 400 && status < 500)
123
+ return 'rejected';
124
+ return 'retry';
125
+ }
126
+ /** A network error (no status at all) is always worth another try. */
127
+ exports.NETWORK_VERDICT = 'retry';
128
+ function applyFailure(entries, verdict, error, now) {
129
+ if (verdict === 'rejected')
130
+ return { keep: [], drop: entries.map((e) => ({ ...e, lastError: error })) };
131
+ const keep = entries.map((entry) => {
132
+ const attempts = entry.attempts + 1;
133
+ return {
134
+ ...entry,
135
+ attempts,
136
+ // A re-auth failure waits on the engineer, not on a timer, but it
137
+ // still backs off so a broken refresh cannot spin on every commit.
138
+ nextAttemptAt: now.getTime() + backoffMs(attempts),
139
+ lastError: error,
140
+ };
141
+ });
142
+ return { keep, drop: [] };
143
+ }
144
+ /** Replaces `original` entries with `replacement` (or removes them). */
145
+ function mergeQueue(queue, original, replacement = []) {
146
+ const removed = new Set(original.map((entry) => entry.id));
147
+ const kept = queue.filter((entry) => !removed.has(entry.id));
148
+ return [...kept, ...replacement];
149
+ }
150
+ // ---------------------------------------------------------------------------
151
+ // Files
152
+ // ---------------------------------------------------------------------------
153
+ function readJsonl(path) {
154
+ let raw;
155
+ try {
156
+ raw = (0, node_fs_1.readFileSync)(path, 'utf8');
157
+ }
158
+ catch {
159
+ return [];
160
+ }
161
+ return raw
162
+ .split('\n')
163
+ .filter((line) => line.trim().length > 0)
164
+ .flatMap((line) => {
165
+ try {
166
+ return [JSON.parse(line)];
167
+ }
168
+ catch {
169
+ // A torn line (power loss mid-append) is one lost commit, not a
170
+ // broken queue.
171
+ return [];
172
+ }
173
+ });
174
+ }
175
+ function readQueue(path) {
176
+ return readJsonl(path);
177
+ }
178
+ /**
179
+ * True when the file's last byte is a newline — false for a file whose last
180
+ * append was cut short, which is the one case where appending blindly would
181
+ * corrupt a *second* line instead of just the torn one.
182
+ */
183
+ function endsWithNewline(path) {
184
+ let handle;
185
+ try {
186
+ const size = (0, node_fs_1.statSync)(path).size;
187
+ if (size === 0)
188
+ return true;
189
+ handle = (0, node_fs_1.openSync)(path, 'r');
190
+ const last = Buffer.alloc(1);
191
+ (0, node_fs_1.readSync)(handle, last, 0, 1, size - 1);
192
+ return last[0] === 0x0a;
193
+ }
194
+ catch {
195
+ return true;
196
+ }
197
+ finally {
198
+ if (handle !== undefined)
199
+ (0, node_fs_1.closeSync)(handle);
200
+ }
201
+ }
202
+ function appendEntry(path, entry) {
203
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
204
+ const prefix = endsWithNewline(path) ? '' : '\n';
205
+ (0, node_fs_1.appendFileSync)(path, `${prefix}${JSON.stringify(entry)}\n`, { mode: 0o600 });
206
+ }
207
+ /** Rewrites the queue atomically — a crash leaves the old file intact. */
208
+ function writeQueue(path, entries) {
209
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
210
+ const temp = `${path}.${process.pid}.tmp`;
211
+ (0, node_fs_1.writeFileSync)(temp, entries.map((entry) => `${JSON.stringify(entry)}\n`).join(''), {
212
+ mode: 0o600,
213
+ });
214
+ (0, node_fs_1.renameSync)(temp, path);
215
+ }
216
+ function appendOutcome(path, outcome) {
217
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
218
+ (0, node_fs_1.appendFileSync)(path, `${JSON.stringify(outcome)}\n`, { mode: 0o600 });
219
+ }
220
+ function readOutcomes(path) {
221
+ return readJsonl(path);
222
+ }
223
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"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"}
package/dist/send.js ADDED
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.payloadForBatch = payloadForBatch;
4
+ exports.payloadForNote = payloadForNote;
5
+ exports.postAmendment = postAmendment;
6
+ exports.flush = flush;
7
+ const queue_1 = require("./queue");
8
+ /** One batch of queued commits as one `/amend` request body. */
9
+ function payloadForBatch(entries) {
10
+ return {
11
+ date: entries[0].day,
12
+ source: 'cli',
13
+ project: entries[0].project,
14
+ commits: entries.map((entry) => entry.commit),
15
+ };
16
+ }
17
+ function payloadForNote(note, day, project) {
18
+ return {
19
+ date: day,
20
+ source: 'manual',
21
+ ...(project && { project }),
22
+ bullets: [note],
23
+ };
24
+ }
25
+ /** One POST. Network failures come back as status 0, not as throws. */
26
+ async function postAmendment(amendUrl, payload, accessToken, fetchImpl = fetch) {
27
+ let response;
28
+ try {
29
+ response = await fetchImpl(amendUrl, {
30
+ method: 'POST',
31
+ headers: {
32
+ authorization: `Bearer ${accessToken}`,
33
+ 'content-type': 'application/json',
34
+ },
35
+ body: JSON.stringify(payload),
36
+ });
37
+ }
38
+ catch (err) {
39
+ return { status: 0, error: err instanceof Error ? err.message : String(err) };
40
+ }
41
+ const text = await response.text();
42
+ let body = {};
43
+ try {
44
+ body = JSON.parse(text);
45
+ }
46
+ catch {
47
+ // Non-JSON error bodies (a proxy, a 502 page) still carry a status.
48
+ }
49
+ return {
50
+ status: response.status,
51
+ amendmentId: typeof body.id === 'string' ? body.id : undefined,
52
+ error: response.ok
53
+ ? undefined
54
+ : typeof body.error === 'string'
55
+ ? body.error
56
+ : text.slice(0, 200) || `HTTP ${response.status}`,
57
+ };
58
+ }
59
+ /**
60
+ * Attempts every due batch once (twice for a 401), persisting the queue
61
+ * after each one so a crash mid-flush cannot resend or lose a batch.
62
+ */
63
+ async function flush({ paths, amendUrl, session, fetchImpl = fetch, now = () => new Date(), log = () => { }, }) {
64
+ const report = {
65
+ sent: 0,
66
+ commitsSent: 0,
67
+ deferred: 0,
68
+ rejected: 0,
69
+ expired: 0,
70
+ needsLogin: false,
71
+ errors: [],
72
+ };
73
+ let queue = (0, queue_1.readQueue)(paths.queue);
74
+ if (queue.length === 0)
75
+ return report;
76
+ const stale = queue.filter((entry) => (0, queue_1.isExpired)(entry, now()));
77
+ if (stale.length > 0) {
78
+ queue = (0, queue_1.mergeQueue)(queue, stale);
79
+ (0, queue_1.writeQueue)(paths.queue, queue);
80
+ (0, queue_1.appendOutcome)(paths.dead, {
81
+ entries: stale,
82
+ at: now().toISOString(),
83
+ status: 0,
84
+ error: `older than the endpoint's ${queue_1.MAX_ENTRY_AGE_DAYS}-day window — never sent`,
85
+ });
86
+ report.expired = stale.length;
87
+ }
88
+ if (!session.isLoggedIn) {
89
+ report.needsLogin = true;
90
+ report.deferred = (0, queue_1.batches)(queue.filter((entry) => (0, queue_1.isDue)(entry, now()))).length;
91
+ return report;
92
+ }
93
+ for (const batch of (0, queue_1.batches)(queue.filter((entry) => (0, queue_1.isDue)(entry, now())))) {
94
+ const payload = payloadForBatch(batch);
95
+ let token;
96
+ try {
97
+ token = await session.accessToken();
98
+ }
99
+ catch {
100
+ report.needsLogin = true;
101
+ report.deferred += 1;
102
+ break;
103
+ }
104
+ let result = await postAmendment(amendUrl, payload, token, fetchImpl);
105
+ let verdict = result.status === 0 ? 'retry' : (0, queue_1.classifyStatus)(result.status);
106
+ if (verdict === 'reauth') {
107
+ // The one failure the CLI can fix by itself — once per batch.
108
+ const refreshed = await session.refresh();
109
+ if (refreshed) {
110
+ result = await postAmendment(amendUrl, payload, refreshed, fetchImpl);
111
+ verdict = result.status === 0 ? 'retry' : (0, queue_1.classifyStatus)(result.status);
112
+ }
113
+ if (verdict === 'reauth')
114
+ report.needsLogin = true;
115
+ }
116
+ if (verdict === 'sent') {
117
+ queue = (0, queue_1.mergeQueue)(queue, batch);
118
+ (0, queue_1.writeQueue)(paths.queue, queue);
119
+ (0, queue_1.appendOutcome)(paths.sent, {
120
+ entries: batch,
121
+ at: now().toISOString(),
122
+ status: result.status,
123
+ amendmentId: result.amendmentId,
124
+ });
125
+ report.sent += 1;
126
+ report.commitsSent += batch.length;
127
+ log(`sent ${batch.length} commit(s) for ${payload.date} (${payload.project})`);
128
+ continue;
129
+ }
130
+ const error = result.error ?? `HTTP ${result.status}`;
131
+ const { keep, drop } = (0, queue_1.applyFailure)(batch, verdict, error, now());
132
+ queue = (0, queue_1.mergeQueue)(queue, batch, keep);
133
+ (0, queue_1.writeQueue)(paths.queue, queue);
134
+ if (drop.length > 0) {
135
+ (0, queue_1.appendOutcome)(paths.dead, {
136
+ entries: drop,
137
+ at: now().toISOString(),
138
+ status: result.status,
139
+ error,
140
+ });
141
+ report.rejected += drop.length;
142
+ }
143
+ else {
144
+ report.deferred += 1;
145
+ }
146
+ report.errors.push(error);
147
+ log(`deferred ${batch.length} commit(s) for ${payload.date}: ${error}`);
148
+ }
149
+ return report;
150
+ }
151
+ //# sourceMappingURL=send.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send.js","sourceRoot":"","sources":["../src/send.ts"],"names":[],"mappings":";;AAmDA,0CAOC;AAED,wCAOC;AASD,sCAmCC;AA8BD,sBAmGC;AA9ND,mCAYiB;AAoBjB,gEAAgE;AAChE,SAAgB,eAAe,CAAC,OAAqB;IACnD,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG;QACpB,MAAM,EAAE,KAAK;QACb,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO;QAC3B,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED,SAAgB,cAAc,CAAC,IAAY,EAAE,GAAW,EAAE,OAAgB;IACxE,OAAO;QACL,IAAI,EAAE,GAAG;QACT,MAAM,EAAE,QAAQ;QAChB,GAAG,CAAC,OAAO,IAAI,EAAE,OAAO,EAAE,CAAC;QAC3B,OAAO,EAAE,CAAC,IAAI,CAAC;KAChB,CAAC;AACJ,CAAC;AAQD,uEAAuE;AAChE,KAAK,UAAU,aAAa,CACjC,QAAgB,EAChB,OAAqB,EACrB,WAAmB,EACnB,YAAmB,KAAK;IAExB,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,SAAS,CAAC,QAAQ,EAAE;YACnC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,WAAW,EAAE;gBACtC,cAAc,EAAE,kBAAkB;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;SAC9B,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IAChF,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,IAAI,IAAI,GAA4B,EAAE,CAAC;IACvC,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;IACtE,CAAC;IACD,OAAO;QACL,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,WAAW,EAAE,OAAO,IAAI,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS;QAC9D,KAAK,EAAE,QAAQ,CAAC,EAAE;YAChB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;gBAC9B,CAAC,CAAC,IAAI,CAAC,KAAK;gBACZ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,QAAQ,QAAQ,CAAC,MAAM,EAAE;KACtD,CAAC;AACJ,CAAC;AA0BD;;;GAGG;AACI,KAAK,UAAU,KAAK,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,OAAO,EACP,SAAS,GAAG,KAAK,EACjB,GAAG,GAAG,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,EACtB,GAAG,GAAG,GAAG,EAAE,GAAE,CAAC,GACD;IACb,MAAM,MAAM,GAAgB;QAC1B,IAAI,EAAE,CAAC;QACP,WAAW,EAAE,CAAC;QACd,QAAQ,EAAE,CAAC;QACX,QAAQ,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;QACV,UAAU,EAAE,KAAK;QACjB,MAAM,EAAE,EAAE;KACX,CAAC;IAEF,IAAI,KAAK,GAAG,IAAA,iBAAS,EAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IAEtC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,iBAAS,EAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IAC/D,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,KAAK,GAAG,IAAA,kBAAU,EAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACjC,IAAA,kBAAU,EAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC/B,IAAA,qBAAa,EAAC,KAAK,CAAC,IAAI,EAAE;YACxB,OAAO,EAAE,KAAK;YACd,EAAE,EAAE,GAAG,EAAE,CAAC,WAAW,EAAE;YACvB,MAAM,EAAE,CAAC;YACT,KAAK,EAAE,6BAA6B,0BAAkB,0BAA0B;SACjF,CAAC,CAAC;QACH,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC;IAChC,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;QACxB,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,MAAM,CAAC,QAAQ,GAAG,IAAA,eAAO,EAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,aAAK,EAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/E,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,IAAA,eAAO,EAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,aAAK,EAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1E,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,KAAa,CAAC;QAClB,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;YACzB,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;YACrB,MAAM;QACR,CAAC;QAED,IAAI,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;QACtE,IAAI,OAAO,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAE,OAAiB,CAAC,CAAC,CAAC,IAAA,sBAAc,EAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAEvF,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,8DAA8D;YAC9D,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;YAC1C,IAAI,SAAS,EAAE,CAAC;gBACd,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;gBACtE,OAAO,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAA,sBAAc,EAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC1E,CAAC;YACD,IAAI,OAAO,KAAK,QAAQ;gBAAE,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;QACrD,CAAC;QAED,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YACvB,KAAK,GAAG,IAAA,kBAAU,EAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACjC,IAAA,kBAAU,EAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC/B,IAAA,qBAAa,EAAC,KAAK,CAAC,IAAI,EAAE;gBACxB,OAAO,EAAE,KAAK;gBACd,EAAE,EAAE,GAAG,EAAE,CAAC,WAAW,EAAE;gBACvB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,WAAW,EAAE,MAAM,CAAC,WAAW;aAChC,CAAC,CAAC;YACH,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC;YACjB,MAAM,CAAC,WAAW,IAAI,KAAK,CAAC,MAAM,CAAC;YACnC,GAAG,CAAC,QAAQ,KAAK,CAAC,MAAM,kBAAkB,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC;YAC/E,SAAS;QACX,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACtD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,IAAA,oBAAY,EAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QAClE,KAAK,GAAG,IAAA,kBAAU,EAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACvC,IAAA,kBAAU,EAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC/B,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpB,IAAA,qBAAa,EAAC,KAAK,CAAC,IAAI,EAAE;gBACxB,OAAO,EAAE,IAAI;gBACb,EAAE,EAAE,GAAG,EAAE,CAAC,WAAW,EAAE;gBACvB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,KAAK;aACN,CAAC,CAAC;YACH,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC;QACjC,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;QACvB,CAAC;QACD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,GAAG,CAAC,YAAY,KAAK,CAAC,MAAM,kBAAkB,OAAO,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC;IAC1E,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/dist/tokens.js ADDED
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Session = exports.NotLoggedInError = void 0;
4
+ exports.storedFrom = storedFrom;
5
+ /**
6
+ * The access token every send needs, refreshed when it has aged out.
7
+ *
8
+ * Refresh happens in two places on purpose: proactively when the stored
9
+ * expiry has passed (cheap, one round trip), and reactively when the
10
+ * endpoint answers 401 anyway — because the CLI's clock, the stored
11
+ * `expires_in` and the issuer's opinion can disagree, and a queue that
12
+ * silently stops sending is exactly the failure mode DATA-ITD-005 is
13
+ * trying to avoid.
14
+ *
15
+ * A refresh that succeeds writes the rotated pair straight back to the
16
+ * keychain: OpenAuth issues a new refresh token on every exchange, so
17
+ * losing the rotated one means the engineer has to sign in again.
18
+ */
19
+ const pkce_1 = require("./pkce");
20
+ class NotLoggedInError extends Error {
21
+ constructor(message = 'not signed in — run `scribe login`') {
22
+ super(message);
23
+ this.name = 'NotLoggedInError';
24
+ }
25
+ }
26
+ exports.NotLoggedInError = NotLoggedInError;
27
+ function storedFrom(tokens, issuer, now) {
28
+ return {
29
+ ...tokens,
30
+ issuer,
31
+ email: (0, pkce_1.readAccessToken)(tokens.access).email,
32
+ savedAt: new Date(now()).toISOString(),
33
+ };
34
+ }
35
+ /**
36
+ * Holds the credentials for one process run: reads them once, refreshes at
37
+ * most once per reason, and persists whatever it rotates.
38
+ */
39
+ class Session {
40
+ issuer;
41
+ store;
42
+ fetchImpl;
43
+ now;
44
+ current;
45
+ constructor({ issuer, store, fetchImpl = fetch, now = Date.now }) {
46
+ this.issuer = issuer;
47
+ this.store = store;
48
+ this.fetchImpl = fetchImpl;
49
+ this.now = now;
50
+ this.current = store.read();
51
+ // Tokens from another stage's issuer are not credentials here.
52
+ if (this.current && this.current.issuer !== issuer)
53
+ this.current = null;
54
+ }
55
+ get email() {
56
+ return this.current?.email ?? null;
57
+ }
58
+ get isLoggedIn() {
59
+ return this.current !== null;
60
+ }
61
+ get expiresAt() {
62
+ return this.current?.expiresAt ?? null;
63
+ }
64
+ /** The token to send, refreshing first if it has expired. */
65
+ async accessToken() {
66
+ if (!this.current)
67
+ throw new NotLoggedInError();
68
+ if ((0, pkce_1.needsRefresh)(this.current, this.now())) {
69
+ const refreshed = await this.refresh();
70
+ if (refreshed)
71
+ return refreshed;
72
+ }
73
+ return this.current.access;
74
+ }
75
+ /**
76
+ * Forces a refresh — what a 401 means — and returns the new access token.
77
+ * Null means the refresh token itself is gone or rejected, the only state
78
+ * that needs the engineer back at a browser.
79
+ */
80
+ async refresh() {
81
+ if (!this.current)
82
+ return null;
83
+ try {
84
+ const rotated = await (0, pkce_1.refreshTokens)({
85
+ issuer: this.issuer,
86
+ refresh: this.current.refresh,
87
+ fetchImpl: this.fetchImpl,
88
+ now: this.now,
89
+ });
90
+ this.current = storedFrom(rotated, this.issuer, this.now);
91
+ this.store.write(this.current);
92
+ return this.current.access;
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+ /** Records a fresh login. */
99
+ save(tokens) {
100
+ this.current = storedFrom(tokens, this.issuer, this.now);
101
+ this.store.write(this.current);
102
+ return this.current;
103
+ }
104
+ }
105
+ exports.Session = Session;
106
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":";;;AA+BA,gCAOC;AAtCD;;;;;;;;;;;;;GAaG;AACH,iCAA+F;AAG/F,MAAa,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAO,GAAG,oCAAoC;QACxD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AALD,4CAKC;AASD,SAAgB,UAAU,CAAC,MAAc,EAAE,MAAc,EAAE,GAAiB;IAC1E,OAAO;QACL,GAAG,MAAM;QACT,MAAM;QACN,KAAK,EAAE,IAAA,sBAAe,EAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK;QAC3C,OAAO,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE;KACvC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAa,OAAO;IACD,MAAM,CAAS;IACf,KAAK,CAAa;IAClB,SAAS,CAAQ;IACjB,GAAG,CAAe;IAC3B,OAAO,CAAsB;IAErC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,KAAK,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAkB;QAC9E,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC5B,+DAA+D;QAC/D,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,MAAM;YAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;IAC1E,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC;IACrC,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC;IAC/B,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,OAAO,EAAE,SAAS,IAAI,IAAI,CAAC;IACzC,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,WAAW;QACf,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,gBAAgB,EAAE,CAAC;QAChD,IAAI,IAAA,mBAAY,EAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;YAC3C,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACvC,IAAI,SAAS;gBAAE,OAAO,SAAS,CAAC;QAClC,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;IAC7B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,IAAA,oBAAa,EAAC;gBAClC,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO;gBAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,GAAG,EAAE,IAAI,CAAC,GAAG;aACd,CAAC,CAAC;YACH,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;YAC1D,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,6BAA6B;IAC7B,IAAI,CAAC,MAAc;QACjB,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACzD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;CACF;AAnED,0BAmEC"}