@gjermundgaraba/clankercreds-api 0.0.0-stage → 0.5.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.
@@ -0,0 +1,144 @@
1
+ import { Schema } from "effect";
2
+ export declare const GhCredential: Schema.Struct<{
3
+ readonly credential: Schema.String;
4
+ readonly expiresAt: Schema.NullOr<Schema.String>;
5
+ readonly type: Schema.Literal<"gh">;
6
+ readonly host: Schema.String;
7
+ readonly token: Schema.String;
8
+ }>;
9
+ export declare const ClaudeCredential: Schema.Struct<{
10
+ readonly credential: Schema.String;
11
+ readonly expiresAt: Schema.NullOr<Schema.String>;
12
+ readonly type: Schema.Literal<"claude">;
13
+ readonly token: Schema.String;
14
+ }>;
15
+ /**
16
+ * What Codex keeps in its `auth.json`, always written together: the three belong to one
17
+ * login. No refresh token: the client writes `<clientId>:<credential>:<key>` in its place.
18
+ */
19
+ export declare const CodexLogin: Schema.Struct<{
20
+ readonly accessToken: Schema.String;
21
+ readonly idToken: Schema.String;
22
+ readonly accountId: Schema.String;
23
+ }>;
24
+ export type CodexLogin = typeof CodexLogin.Type;
25
+ export declare const CodexCredential: Schema.Struct<{
26
+ readonly credential: Schema.String;
27
+ readonly expiresAt: Schema.NullOr<Schema.String>;
28
+ readonly accessToken: Schema.String;
29
+ readonly idToken: Schema.String;
30
+ readonly accountId: Schema.String;
31
+ readonly type: Schema.Literal<"codex">;
32
+ }>;
33
+ /**
34
+ * The access token `clankercreds token` answers pi with until it is near expiry: pi asks the
35
+ * command on every model call, and the command asks the service only after that.
36
+ */
37
+ export declare const PiCredential: Schema.Struct<{
38
+ readonly credential: Schema.String;
39
+ readonly expiresAt: Schema.NullOr<Schema.String>;
40
+ readonly type: Schema.Literal<"pi">;
41
+ readonly accessToken: Schema.String;
42
+ }>;
43
+ /**
44
+ * A credential no machine places: an API key, or a Grok or Cursor login's access token, for
45
+ * a service such as clankerusage. `kind` is the credential's own, and any string, so a kind
46
+ * newer than the caller's contract still decodes. The client skips one a config names.
47
+ */
48
+ export declare const TokenCredential: Schema.Struct<{
49
+ readonly credential: Schema.String;
50
+ readonly expiresAt: Schema.NullOr<Schema.String>;
51
+ readonly type: Schema.Literal<"token">;
52
+ readonly kind: Schema.String;
53
+ readonly token: Schema.String;
54
+ }>;
55
+ /**
56
+ * The service sends typed credentials, never file contents or paths, so it cannot write
57
+ * anything on a machine but what the client knows how to place. The cost is that a new
58
+ * tool, or a placement fix, needs a client release, which new machines install. Each entry
59
+ * names the credential it came from: a ChatGPT login arrives as a `codex` and a `pi` entry
60
+ * under the same name.
61
+ *
62
+ * Each machine keeps the client its setup installed, so old clients read what the service sends. A change
63
+ * an old client would get wrong is a new type name, which it skips: the decoder drops fields
64
+ * it does not know, so one ignoring `host` would place a GHE token as github.com's. A field
65
+ * an old client can safely ignore may be added; the service ships first, since a new client
66
+ * refuses an entry that lacks it.
67
+ */
68
+ export declare const TypedCredential: Schema.Union<readonly [Schema.Struct<{
69
+ readonly credential: Schema.String;
70
+ readonly expiresAt: Schema.NullOr<Schema.String>;
71
+ readonly type: Schema.Literal<"gh">;
72
+ readonly host: Schema.String;
73
+ readonly token: Schema.String;
74
+ }>, Schema.Struct<{
75
+ readonly credential: Schema.String;
76
+ readonly expiresAt: Schema.NullOr<Schema.String>;
77
+ readonly type: Schema.Literal<"claude">;
78
+ readonly token: Schema.String;
79
+ }>, Schema.Struct<{
80
+ readonly credential: Schema.String;
81
+ readonly expiresAt: Schema.NullOr<Schema.String>;
82
+ readonly accessToken: Schema.String;
83
+ readonly idToken: Schema.String;
84
+ readonly accountId: Schema.String;
85
+ readonly type: Schema.Literal<"codex">;
86
+ }>, Schema.Struct<{
87
+ readonly credential: Schema.String;
88
+ readonly expiresAt: Schema.NullOr<Schema.String>;
89
+ readonly type: Schema.Literal<"pi">;
90
+ readonly accessToken: Schema.String;
91
+ }>, Schema.Struct<{
92
+ readonly credential: Schema.String;
93
+ readonly expiresAt: Schema.NullOr<Schema.String>;
94
+ readonly type: Schema.Literal<"token">;
95
+ readonly kind: Schema.String;
96
+ readonly token: Schema.String;
97
+ }>]>;
98
+ export type TypedCredential = typeof TypedCredential.Type;
99
+ /** The types this build knows, derived from the union so there is one list. */
100
+ export declare const knownTypes: ReadonlyArray<TypedCredential["type"]>;
101
+ export declare const isKnownType: (type: string) => type is TypedCredential["type"];
102
+ /**
103
+ * What the service may send that this client does not know yet. It decodes instead of
104
+ * failing, so the client can skip it with a warning. A known type that fails its own
105
+ * schema must not land here, or a broken credential would be placed as an empty one.
106
+ */
107
+ export declare const UnknownCredential: Schema.Struct<{
108
+ readonly type: Schema.String;
109
+ }>;
110
+ /** Everything the caller asked for, sent in full on every sync. */
111
+ export declare const Delivery: Schema.Struct<{
112
+ readonly credentials: Schema.$Array<Schema.Union<readonly [Schema.Union<readonly [Schema.Struct<{
113
+ readonly credential: Schema.String;
114
+ readonly expiresAt: Schema.NullOr<Schema.String>;
115
+ readonly type: Schema.Literal<"gh">;
116
+ readonly host: Schema.String;
117
+ readonly token: Schema.String;
118
+ }>, Schema.Struct<{
119
+ readonly credential: Schema.String;
120
+ readonly expiresAt: Schema.NullOr<Schema.String>;
121
+ readonly type: Schema.Literal<"claude">;
122
+ readonly token: Schema.String;
123
+ }>, Schema.Struct<{
124
+ readonly credential: Schema.String;
125
+ readonly expiresAt: Schema.NullOr<Schema.String>;
126
+ readonly accessToken: Schema.String;
127
+ readonly idToken: Schema.String;
128
+ readonly accountId: Schema.String;
129
+ readonly type: Schema.Literal<"codex">;
130
+ }>, Schema.Struct<{
131
+ readonly credential: Schema.String;
132
+ readonly expiresAt: Schema.NullOr<Schema.String>;
133
+ readonly type: Schema.Literal<"pi">;
134
+ readonly accessToken: Schema.String;
135
+ }>, Schema.Struct<{
136
+ readonly credential: Schema.String;
137
+ readonly expiresAt: Schema.NullOr<Schema.String>;
138
+ readonly type: Schema.Literal<"token">;
139
+ readonly kind: Schema.String;
140
+ readonly token: Schema.String;
141
+ }>]>, Schema.Struct<{
142
+ readonly type: Schema.String;
143
+ }>]>>;
144
+ }>;
@@ -0,0 +1,91 @@
1
+ import { Schema } from "effect";
2
+ import { Name } from "./shared.js";
3
+ /**
4
+ * The credential an entry was delivered from, as it was asked for by name, and when what it
5
+ * carries expires, as ISO: a secret's as the operator entered it, a login's access token's
6
+ * own. Null never expires. A login delivered past it could not be renewed in time, as when its
7
+ * provider is away: sync again later. Whether it needs a new sign-in is the operator's to see.
8
+ */
9
+ const fromCredential = { credential: Name, expiresAt: Schema.NullOr(Schema.String) };
10
+ export const GhCredential = Schema.Struct({
11
+ type: Schema.Literal("gh"),
12
+ ...fromCredential,
13
+ host: Schema.String,
14
+ token: Schema.String,
15
+ });
16
+ export const ClaudeCredential = Schema.Struct({
17
+ type: Schema.Literal("claude"),
18
+ ...fromCredential,
19
+ token: Schema.String,
20
+ });
21
+ /**
22
+ * What Codex keeps in its `auth.json`, always written together: the three belong to one
23
+ * login. No refresh token: the client writes `<clientId>:<credential>:<key>` in its place.
24
+ */
25
+ export const CodexLogin = Schema.Struct({
26
+ accessToken: Schema.String,
27
+ idToken: Schema.String,
28
+ accountId: Schema.String,
29
+ });
30
+ export const CodexCredential = Schema.Struct({
31
+ type: Schema.Literal("codex"),
32
+ ...fromCredential,
33
+ ...CodexLogin.fields,
34
+ });
35
+ /**
36
+ * The access token `clankercreds token` answers pi with until it is near expiry: pi asks the
37
+ * command on every model call, and the command asks the service only after that.
38
+ */
39
+ export const PiCredential = Schema.Struct({
40
+ type: Schema.Literal("pi"),
41
+ ...fromCredential,
42
+ accessToken: Schema.String,
43
+ });
44
+ /**
45
+ * A credential no machine places: an API key, or a Grok or Cursor login's access token, for
46
+ * a service such as clankerusage. `kind` is the credential's own, and any string, so a kind
47
+ * newer than the caller's contract still decodes. The client skips one a config names.
48
+ */
49
+ export const TokenCredential = Schema.Struct({
50
+ type: Schema.Literal("token"),
51
+ ...fromCredential,
52
+ kind: Schema.String,
53
+ token: Schema.String,
54
+ });
55
+ /**
56
+ * The service sends typed credentials, never file contents or paths, so it cannot write
57
+ * anything on a machine but what the client knows how to place. The cost is that a new
58
+ * tool, or a placement fix, needs a client release, which new machines install. Each entry
59
+ * names the credential it came from: a ChatGPT login arrives as a `codex` and a `pi` entry
60
+ * under the same name.
61
+ *
62
+ * Each machine keeps the client its setup installed, so old clients read what the service sends. A change
63
+ * an old client would get wrong is a new type name, which it skips: the decoder drops fields
64
+ * it does not know, so one ignoring `host` would place a GHE token as github.com's. A field
65
+ * an old client can safely ignore may be added; the service ships first, since a new client
66
+ * refuses an entry that lacks it.
67
+ */
68
+ export const TypedCredential = Schema.Union([
69
+ GhCredential,
70
+ ClaudeCredential,
71
+ CodexCredential,
72
+ PiCredential,
73
+ TokenCredential,
74
+ ]);
75
+ /** The types this build knows, derived from the union so there is one list. */
76
+ export const knownTypes = TypedCredential.members.map((member) => member.fields.type.literal);
77
+ export const isKnownType = (type) => knownTypes.some((known) => known === type);
78
+ /**
79
+ * What the service may send that this client does not know yet. It decodes instead of
80
+ * failing, so the client can skip it with a warning. A known type that fails its own
81
+ * schema must not land here, or a broken credential would be placed as an empty one.
82
+ */
83
+ export const UnknownCredential = Schema.Struct({
84
+ type: Schema.String.check(Schema.makeFilter((type) => !isKnownType(type), {
85
+ message: "a known credential type must match its schema",
86
+ })),
87
+ });
88
+ /** Everything the caller asked for, sent in full on every sync. */
89
+ export const Delivery = Schema.Struct({
90
+ credentials: Schema.Array(Schema.Union([TypedCredential, UnknownCredential])),
91
+ });
@@ -0,0 +1,180 @@
1
+ import { Schema } from "effect";
2
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
3
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
4
+ import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
5
+ import { ProviderUnavailable } from "@gjermundgaraba/clankerauth-sdk/errors";
6
+ import { CurrentPrincipal } from "@gjermundgaraba/clankerauth-sdk/session";
7
+ import { claimsOf, ClientId, CredentialKind, keyIdOf, Name, NeedsSignIn, NotFound, UpstreamUnavailable } from "./shared.ts";
8
+ /**
9
+ * A credential the calling key may read, without its value. `kind` is any string, so a kind
10
+ * newer than the caller's contract does not fail the whole list.
11
+ */
12
+ export declare const Grant: Schema.Struct<{
13
+ readonly name: Schema.String;
14
+ readonly kind: Schema.String;
15
+ }>;
16
+ /**
17
+ * What a machine key can do: read the credentials it is granted, by name, and nothing else.
18
+ * The caller names what it wants; the grants on the server decide whether it may. A machine
19
+ * is one caller; a service such as clankerusage, holding a key of its own, is another.
20
+ */
21
+ export declare const Machine: readonly [Action.Action<"sync", Schema.Struct<{
22
+ readonly clientId: Schema.String;
23
+ readonly credentials: Schema.$Array<Schema.String>;
24
+ }>, Schema.Struct<{
25
+ readonly credentials: Schema.$Array<Schema.Union<readonly [Schema.Union<readonly [Schema.Struct<{
26
+ readonly credential: Schema.String;
27
+ readonly expiresAt: Schema.NullOr<Schema.String>;
28
+ readonly type: Schema.Literal<"gh">;
29
+ readonly host: Schema.String;
30
+ readonly token: Schema.String;
31
+ }>, Schema.Struct<{
32
+ readonly credential: Schema.String;
33
+ readonly expiresAt: Schema.NullOr<Schema.String>;
34
+ readonly type: Schema.Literal<"claude">;
35
+ readonly token: Schema.String;
36
+ }>, Schema.Struct<{
37
+ readonly credential: Schema.String;
38
+ readonly expiresAt: Schema.NullOr<Schema.String>;
39
+ readonly accessToken: Schema.String;
40
+ readonly idToken: Schema.String;
41
+ readonly accountId: Schema.String;
42
+ readonly type: Schema.Literal<"codex">;
43
+ }>, Schema.Struct<{
44
+ readonly credential: Schema.String;
45
+ readonly expiresAt: Schema.NullOr<Schema.String>;
46
+ readonly type: Schema.Literal<"pi">;
47
+ readonly accessToken: Schema.String;
48
+ }>, Schema.Struct<{
49
+ readonly credential: Schema.String;
50
+ readonly expiresAt: Schema.NullOr<Schema.String>;
51
+ readonly type: Schema.Literal<"token">;
52
+ readonly kind: Schema.String;
53
+ readonly token: Schema.String;
54
+ }>]>, Schema.Struct<{
55
+ readonly type: Schema.String;
56
+ }>]>>;
57
+ }>, readonly (typeof NotFound)[], false, typeof CurrentPrincipal>, Action.Action<"codexLogin", Schema.Struct<{
58
+ readonly clientId: Schema.String;
59
+ readonly credential: Schema.String;
60
+ }>, Schema.Struct<{
61
+ readonly accessToken: Schema.String;
62
+ readonly idToken: Schema.String;
63
+ readonly accountId: Schema.String;
64
+ }>, readonly (typeof NotFound | typeof NeedsSignIn | typeof UpstreamUnavailable)[], false, typeof CurrentPrincipal>, Action.Action<"listGrants", Schema.$Record<Schema.String, Schema.Never>, Schema.Struct<{
65
+ readonly credentials: Schema.$Array<Schema.Struct<{
66
+ readonly name: Schema.String;
67
+ readonly kind: Schema.String;
68
+ }>>;
69
+ }>, readonly never[], true, typeof CurrentPrincipal>];
70
+ declare const CodexRefreshRejected_base: Schema.Class<CodexRefreshRejected, Schema.TaggedStruct<"CodexRefreshRejected", {
71
+ readonly error: Schema.Struct<{
72
+ readonly code: Schema.String;
73
+ readonly message: Schema.String;
74
+ }>;
75
+ }>, import("effect/Cause").YieldableError>;
76
+ /**
77
+ * Codex treats a 401 as permanent and stops retrying; every other failure is transient.
78
+ * The codes are the ones Codex classifies: `refresh_token_invalidated` reads as revoked.
79
+ */
80
+ export declare class CodexRefreshRejected extends CodexRefreshRejected_base {
81
+ }
82
+ declare const CodexRefreshUnavailable_base: Schema.Class<CodexRefreshUnavailable, Schema.TaggedStruct<"CodexRefreshUnavailable", {
83
+ readonly error: Schema.Struct<{
84
+ readonly code: Schema.String;
85
+ readonly message: Schema.String;
86
+ }>;
87
+ }>, import("effect/Cause").YieldableError>;
88
+ export declare class CodexRefreshUnavailable extends CodexRefreshUnavailable_base {
89
+ }
90
+ /**
91
+ * Codex's own refresh request, the one request whose shape is not ours. The refresh-token
92
+ * field carries `<clientId>:<credential>:<key>`, so the contract is public and its handler authenticates
93
+ * the key against the machine resource. The answer leaves `refresh_token` out, so Codex keeps the
94
+ * composite it has. Its input alone accepts fields it does not name: Codex may add one,
95
+ * and every other action refuses them.
96
+ */
97
+ export declare const CodexRefresh: Action.Action<"codexRefresh", Schema.StructWithRest<Schema.Struct<{
98
+ readonly grant_type: Schema.Literal<"refresh_token">;
99
+ readonly refresh_token: Schema.String;
100
+ readonly client_id: Schema.optionalKey<Schema.String>;
101
+ readonly scope: Schema.optionalKey<Schema.String>;
102
+ }>, readonly [Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>]>, Schema.Struct<{
103
+ readonly access_token: Schema.String;
104
+ readonly id_token: Schema.String;
105
+ }>, readonly (typeof CodexRefreshRejected | typeof CodexRefreshUnavailable)[], false, typeof Action.Anyone>;
106
+ /**
107
+ * `<clientId>:<credential>:<key>`: a key may be granted several ChatGPT logins, so the
108
+ * composite names the one Codex holds. Client ids and names exclude `:`, so the key is the rest.
109
+ */
110
+ export declare const formatCodexRefreshToken: (clientId: string, credential: string, key: string) => string;
111
+ export declare const parseCodexRefreshToken: (value: string) => {
112
+ clientId: string;
113
+ credential: string;
114
+ key: string;
115
+ } | undefined;
116
+ /**
117
+ * A machine key's bearer token on the public port, verified by the machine resource's own
118
+ * provider, which requires `clankercreds:machine`: a key without that scope is refused here.
119
+ */
120
+ export declare const MachineLogin: Authentication.Descriptor<CurrentPrincipal, import("@gjermundgaraba/clankerauth-sdk/session").Caller, import("effect/http-api/HttpApiSecurity").Http, "clankercreds.machine", readonly [typeof ProviderUnavailable]>;
121
+ /** Everything the public listener serves: the machine actions, and Codex's refresh beside them. */
122
+ export declare const MachineHttp: ActionHttp.Binding<readonly [Action.Action<"sync", Schema.Struct<{
123
+ readonly clientId: Schema.String;
124
+ readonly credentials: Schema.$Array<Schema.String>;
125
+ }>, Schema.Struct<{
126
+ readonly credentials: Schema.$Array<Schema.Union<readonly [Schema.Union<readonly [Schema.Struct<{
127
+ readonly credential: Schema.String;
128
+ readonly expiresAt: Schema.NullOr<Schema.String>;
129
+ readonly type: Schema.Literal<"gh">;
130
+ readonly host: Schema.String;
131
+ readonly token: Schema.String;
132
+ }>, Schema.Struct<{
133
+ readonly credential: Schema.String;
134
+ readonly expiresAt: Schema.NullOr<Schema.String>;
135
+ readonly type: Schema.Literal<"claude">;
136
+ readonly token: Schema.String;
137
+ }>, Schema.Struct<{
138
+ readonly credential: Schema.String;
139
+ readonly expiresAt: Schema.NullOr<Schema.String>;
140
+ readonly accessToken: Schema.String;
141
+ readonly idToken: Schema.String;
142
+ readonly accountId: Schema.String;
143
+ readonly type: Schema.Literal<"codex">;
144
+ }>, Schema.Struct<{
145
+ readonly credential: Schema.String;
146
+ readonly expiresAt: Schema.NullOr<Schema.String>;
147
+ readonly type: Schema.Literal<"pi">;
148
+ readonly accessToken: Schema.String;
149
+ }>, Schema.Struct<{
150
+ readonly credential: Schema.String;
151
+ readonly expiresAt: Schema.NullOr<Schema.String>;
152
+ readonly type: Schema.Literal<"token">;
153
+ readonly kind: Schema.String;
154
+ readonly token: Schema.String;
155
+ }>]>, Schema.Struct<{
156
+ readonly type: Schema.String;
157
+ }>]>>;
158
+ }>, readonly (typeof NotFound)[], false, typeof CurrentPrincipal>, Action.Action<"codexLogin", Schema.Struct<{
159
+ readonly clientId: Schema.String;
160
+ readonly credential: Schema.String;
161
+ }>, Schema.Struct<{
162
+ readonly accessToken: Schema.String;
163
+ readonly idToken: Schema.String;
164
+ readonly accountId: Schema.String;
165
+ }>, readonly (typeof NotFound | typeof NeedsSignIn | typeof UpstreamUnavailable)[], false, typeof CurrentPrincipal>, Action.Action<"listGrants", Schema.$Record<Schema.String, Schema.Never>, Schema.Struct<{
166
+ readonly credentials: Schema.$Array<Schema.Struct<{
167
+ readonly name: Schema.String;
168
+ readonly kind: Schema.String;
169
+ }>>;
170
+ }>, readonly never[], true, typeof CurrentPrincipal>, Action.Action<"codexRefresh", Schema.StructWithRest<Schema.Struct<{
171
+ readonly grant_type: Schema.Literal<"refresh_token">;
172
+ readonly refresh_token: Schema.String;
173
+ readonly client_id: Schema.optionalKey<Schema.String>;
174
+ readonly scope: Schema.optionalKey<Schema.String>;
175
+ }>, readonly [Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>]>, Schema.Struct<{
176
+ readonly access_token: Schema.String;
177
+ readonly id_token: Schema.String;
178
+ }>, readonly (typeof CodexRefreshRejected | typeof CodexRefreshUnavailable)[], false, typeof Action.Anyone>], [], Authentication.Descriptor<CurrentPrincipal, import("@gjermundgaraba/clankerauth-sdk/session").Caller, import("effect/http-api/HttpApiSecurity").Http, "clankercreds.machine", readonly [typeof ProviderUnavailable]>>;
179
+ export { claimsOf, ClientId, CredentialKind, keyIdOf, Name, NeedsSignIn, NotFound, UpstreamUnavailable, };
180
+ export * from "./credentials.ts";
@@ -0,0 +1,102 @@
1
+ import { Schema } from "effect";
2
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
3
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
4
+ import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
5
+ import { ProviderUnavailable } from "@gjermundgaraba/clankerauth-sdk/errors";
6
+ import { CurrentPrincipal } from "@gjermundgaraba/clankerauth-sdk/session";
7
+ import { CodexLogin, Delivery } from "./credentials.js";
8
+ import { apiPath, claimsOf, ClientId, CredentialKind, keyIdOf, Name, NeedsSignIn, NotFound, UpstreamUnavailable, } from "./shared.js";
9
+ /**
10
+ * A credential the calling key may read, without its value. `kind` is any string, so a kind
11
+ * newer than the caller's contract does not fail the whole list.
12
+ */
13
+ export const Grant = Schema.Struct({ name: Name, kind: Schema.String });
14
+ /**
15
+ * What a machine key can do: read the credentials it is granted, by name, and nothing else.
16
+ * The caller names what it wants; the grants on the server decide whether it may. A machine
17
+ * is one caller; a service such as clankerusage, holding a key of its own, is another.
18
+ */
19
+ export const Machine = [
20
+ Action.make("sync", {
21
+ // Writes an audit row, served or refused; may save a refreshed login.
22
+ readOnly: false,
23
+ caller: CurrentPrincipal,
24
+ description: "Fetch the named credentials as typed entries, each with its expiry; a login near expiry is refreshed first. Refused whole when the calling key is not granted any one of them.",
25
+ input: Schema.Struct({ clientId: ClientId, credentials: Schema.Array(Name) }),
26
+ success: Delivery,
27
+ error: NotFound,
28
+ }),
29
+ Action.make("codexLogin", {
30
+ // May save a refreshed login; writes an audit row.
31
+ readOnly: false,
32
+ caller: CurrentPrincipal,
33
+ description: "The current login of a ChatGPT credential the calling key is granted, as the codex type carries it, refreshed first when it has expired.",
34
+ input: Schema.Struct({ clientId: ClientId, credential: Name }),
35
+ success: CodexLogin,
36
+ error: [NotFound, NeedsSignIn, UpstreamUnavailable],
37
+ }),
38
+ Action.make("listGrants", {
39
+ readOnly: true,
40
+ caller: CurrentPrincipal,
41
+ description: "The names and kinds of the credentials the calling key is granted, so a caller can offer them to pick from. Values are never returned.",
42
+ success: Schema.Struct({ credentials: Schema.Array(Grant) }),
43
+ }),
44
+ ];
45
+ /**
46
+ * Codex treats a 401 as permanent and stops retrying; every other failure is transient.
47
+ * The codes are the ones Codex classifies: `refresh_token_invalidated` reads as revoked.
48
+ */
49
+ export class CodexRefreshRejected extends Schema.TaggedError()("CodexRefreshRejected", { error: Schema.Struct({ code: Schema.String, message: Schema.String }) }, { httpApiStatus: 401 }) {
50
+ }
51
+ export class CodexRefreshUnavailable extends Schema.TaggedError()("CodexRefreshUnavailable", { error: Schema.Struct({ code: Schema.String, message: Schema.String }) }, { httpApiStatus: 503 }) {
52
+ }
53
+ /**
54
+ * Codex's own refresh request, the one request whose shape is not ours. The refresh-token
55
+ * field carries `<clientId>:<credential>:<key>`, so the contract is public and its handler authenticates
56
+ * the key against the machine resource. The answer leaves `refresh_token` out, so Codex keeps the
57
+ * composite it has. Its input alone accepts fields it does not name: Codex may add one,
58
+ * and every other action refuses them.
59
+ */
60
+ export const CodexRefresh = Action.make("codexRefresh", {
61
+ // May save a refreshed login; a served login writes an audit row.
62
+ readOnly: false,
63
+ caller: Action.Anyone,
64
+ description: "Answer Codex's token refresh with the current tokens of the ChatGPT credential its composite names.",
65
+ input: Schema.StructWithRest(Schema.Struct({
66
+ grant_type: Schema.Literal("refresh_token"),
67
+ refresh_token: Schema.String,
68
+ client_id: Schema.optionalKey(Schema.String),
69
+ scope: Schema.optionalKey(Schema.String),
70
+ }), [Schema.Record(Schema.String, Schema.Json)]),
71
+ success: Schema.Struct({ access_token: Schema.String, id_token: Schema.String }),
72
+ error: [CodexRefreshRejected, CodexRefreshUnavailable],
73
+ });
74
+ /**
75
+ * `<clientId>:<credential>:<key>`: a key may be granted several ChatGPT logins, so the
76
+ * composite names the one Codex holds. Client ids and names exclude `:`, so the key is the rest.
77
+ */
78
+ export const formatCodexRefreshToken = (clientId, credential, key) => `${clientId}:${credential}:${key}`;
79
+ export const parseCodexRefreshToken = (value) => {
80
+ const first = value.indexOf(":");
81
+ const second = value.indexOf(":", first + 1);
82
+ if (first < 1 || second === -1 || second === value.length - 1)
83
+ return undefined;
84
+ const credential = value.slice(first + 1, second);
85
+ return Schema.is(Name)(credential)
86
+ ? { clientId: value.slice(0, first), credential, key: value.slice(second + 1) }
87
+ : undefined;
88
+ };
89
+ /**
90
+ * A machine key's bearer token on the public port, verified by the machine resource's own
91
+ * provider, which requires `clankercreds:machine`: a key without that scope is refused here.
92
+ */
93
+ export const MachineLogin = Authentication.make("clankercreds.machine", CurrentPrincipal, {
94
+ error: ProviderUnavailable,
95
+ });
96
+ /** Everything the public listener serves: the machine actions, and Codex's refresh beside them. */
97
+ export const MachineHttp = ActionHttp.make([...Machine, CodexRefresh], {
98
+ prefix: apiPath,
99
+ authentication: MachineLogin,
100
+ });
101
+ export { claimsOf, ClientId, CredentialKind, keyIdOf, Name, NeedsSignIn, NotFound, UpstreamUnavailable, };
102
+ export * from "./credentials.js";
@@ -0,0 +1,84 @@
1
+ import { Schema } from "effect";
2
+ import type { Caller } from "@gjermundgaraba/clankerauth-sdk/session";
3
+ /** Every route is `POST /v1/<action>`: both bindings mount here, each on its own listener. */
4
+ export declare const apiPath = "/v1";
5
+ /**
6
+ * What a credential of the clankercreds resource may carry. `machine` admits the
7
+ * public actions and nothing else; `read` and `admin` admit the admin actions.
8
+ */
9
+ export declare const scopes: {
10
+ readonly machine: "clankercreds:machine";
11
+ readonly read: "clankercreds:read";
12
+ readonly admin: "clankercreds:admin";
13
+ };
14
+ declare const NotFound_base: Schema.Class<NotFound, Schema.TaggedStruct<"NotFound", {
15
+ readonly message: Schema.String;
16
+ }>, import("effect/Cause").YieldableError>;
17
+ /**
18
+ * What was asked for does not exist: a key, credential or sign-in by that name, or a
19
+ * credential the calling machine key is not granted, which it is not told apart from one
20
+ * that does not exist.
21
+ */
22
+ export declare class NotFound extends NotFound_base {
23
+ }
24
+ declare const Conflict_base: Schema.Class<Conflict, Schema.TaggedStruct<"Conflict", {
25
+ readonly message: Schema.String;
26
+ }>, import("effect/Cause").YieldableError>;
27
+ /** The change collides with what is stored: a taken name, or a credential still in use. */
28
+ export declare class Conflict extends Conflict_base {
29
+ }
30
+ declare const NeedsSignIn_base: Schema.Class<NeedsSignIn, Schema.TaggedStruct<"NeedsSignIn", {
31
+ readonly message: Schema.String;
32
+ }>, import("effect/Cause").YieldableError>;
33
+ /** The login is dead, or was never completed. Only a new sign-in revives it. */
34
+ export declare class NeedsSignIn extends NeedsSignIn_base {
35
+ }
36
+ declare const UpstreamUnavailable_base: Schema.Class<UpstreamUnavailable, Schema.TaggedStruct<"UpstreamUnavailable", {
37
+ readonly message: Schema.String;
38
+ }>, import("effect/Cause").YieldableError>;
39
+ /** The provider could not be reached and no valid token is held. Waiting helps; retrying now does not. */
40
+ export declare class UpstreamUnavailable extends UpstreamUnavailable_base {
41
+ }
42
+ /**
43
+ * Credential names: lowercase labels, because they are typed by hand and are
44
+ * identities. SQLite compares them case-sensitively, so mixed case would let `GitHub-Main`
45
+ * and `github-main` be two credentials.
46
+ */
47
+ export declare const Name: Schema.String;
48
+ /**
49
+ * What an operator stores a value for: a GitHub token, a Claude token, or `token`, any other
50
+ * service's API key, which a service such as clankerusage reads and no machine places.
51
+ */
52
+ export declare const SecretKind: Schema.Literals<readonly ["gh", "claude", "token"]>;
53
+ /**
54
+ * What a sign-in creates and the service keeps refreshed. The refresh token never leaves
55
+ * the service; only access tokens are delivered.
56
+ */
57
+ export declare const LoginKind: Schema.Literals<readonly ["chatgpt", "grok", "cursor"]>;
58
+ export declare const CredentialKind: Schema.Literals<readonly ["gh", "claude", "token", "chatgpt", "grok", "cursor"]>;
59
+ /** A GitHub host, lowercase: it is written into `gh`'s and git's configuration as is. */
60
+ export declare const Host: Schema.String;
61
+ /**
62
+ * The machine's self-generated label. It identifies nothing; it only groups audit rows.
63
+ * It excludes `:` because the Codex refresh token is `<clientId>:<credential>:<key>`.
64
+ */
65
+ export declare const ClientId: Schema.String;
66
+ /**
67
+ * The one id the service records for a caller, whichever way clankerauth names it: the
68
+ * process itself, which no surface here admits, by its subject.
69
+ */
70
+ export declare const keyIdOf: ({ actor, subject }: Caller) => string;
71
+ /**
72
+ * The claims of a JWT, unverified: they are read for their timing, email and, in OpenAI's,
73
+ * account, never trusted for anything. Anything unreadable is simply no claims.
74
+ */
75
+ export declare const claimsOf: (jwt: string) => {
76
+ exp: undefined;
77
+ email: undefined;
78
+ accountId: undefined;
79
+ } | {
80
+ exp: number | undefined;
81
+ email: string | undefined;
82
+ accountId: string | undefined;
83
+ };
84
+ export {};