@ankhorage/contracts 2.0.0 → 3.0.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 +12 -0
- package/dist/auth.d.ts +80 -14
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +37 -0
- package/dist/auth.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/secretManifest.d.ts +11 -0
- package/dist/secretManifest.d.ts.map +1 -0
- package/dist/secretManifest.js +2 -0
- package/dist/secretManifest.js.map +1 -0
- package/dist/secrets.d.ts +85 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +83 -0
- package/dist/secrets.js.map +1 -0
- package/package.json +8 -2
- package/src/auth-oauth.test.ts +189 -44
- package/src/auth.ts +135 -16
- package/src/index.ts +2 -0
- package/src/secretManifest.ts +12 -0
- package/src/secrets.test.ts +88 -0
- package/src/secrets.ts +188 -0
package/src/secrets.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export const SECRET_STORE_PROVIDERS = ['supabase-vault'] as const;
|
|
2
|
+
export type KnownSecretStoreProvider = (typeof SECRET_STORE_PROVIDERS)[number];
|
|
3
|
+
export type SecretStoreProvider = KnownSecretStoreProvider | (string & {});
|
|
4
|
+
|
|
5
|
+
export type SecretRef = string;
|
|
6
|
+
|
|
7
|
+
export interface SecretScope {
|
|
8
|
+
projectId: string;
|
|
9
|
+
environment: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type SecretPayload = Readonly<Record<string, string>>;
|
|
13
|
+
|
|
14
|
+
export interface SecretMetadata {
|
|
15
|
+
ref: SecretRef;
|
|
16
|
+
scope: SecretScope;
|
|
17
|
+
kind: string;
|
|
18
|
+
provider?: string;
|
|
19
|
+
configuredFields: readonly string[];
|
|
20
|
+
createdAt: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const SECRET_STORE_ERROR_CODES = [
|
|
25
|
+
'invalid_config',
|
|
26
|
+
'invalid_reference',
|
|
27
|
+
'invalid_payload',
|
|
28
|
+
'not_found',
|
|
29
|
+
'conflict',
|
|
30
|
+
'permission_denied',
|
|
31
|
+
'unavailable',
|
|
32
|
+
'provider_error',
|
|
33
|
+
] as const;
|
|
34
|
+
export type SecretStoreErrorCode = (typeof SECRET_STORE_ERROR_CODES)[number];
|
|
35
|
+
|
|
36
|
+
export interface SecretStoreError {
|
|
37
|
+
code: SecretStoreErrorCode;
|
|
38
|
+
message: string;
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SecretStoreOkResult<TData> = [TData] extends [void]
|
|
43
|
+
? { ok: true; data?: undefined }
|
|
44
|
+
: { ok: true; data: TData };
|
|
45
|
+
|
|
46
|
+
export type SecretStoreResult<TData = void> =
|
|
47
|
+
| SecretStoreOkResult<TData>
|
|
48
|
+
| {
|
|
49
|
+
ok: false;
|
|
50
|
+
error: SecretStoreError;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export interface SecretListInput {
|
|
54
|
+
scope: SecretScope;
|
|
55
|
+
kind?: string;
|
|
56
|
+
provider?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SecretGetMetadataInput {
|
|
60
|
+
scope: SecretScope;
|
|
61
|
+
ref: SecretRef;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SecretCreateInput {
|
|
65
|
+
scope: SecretScope;
|
|
66
|
+
ref: SecretRef;
|
|
67
|
+
kind: string;
|
|
68
|
+
provider?: string;
|
|
69
|
+
payload: SecretPayload;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SecretReplaceInput {
|
|
73
|
+
scope: SecretScope;
|
|
74
|
+
ref: SecretRef;
|
|
75
|
+
payload: SecretPayload;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SecretRemoveInput {
|
|
79
|
+
scope: SecretScope;
|
|
80
|
+
ref: SecretRef;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SecretResolveInput {
|
|
84
|
+
scope: SecretScope;
|
|
85
|
+
ref: SecretRef;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Provider-neutral server-side secret-store boundary.
|
|
90
|
+
*
|
|
91
|
+
* `resolve` is for trusted server/deployment code only. Browser bridges must expose
|
|
92
|
+
* metadata operations without forwarding raw secret payloads.
|
|
93
|
+
*/
|
|
94
|
+
export interface SecretStoreAdapter {
|
|
95
|
+
list(input: SecretListInput): Promise<SecretStoreResult<readonly SecretMetadata[]>>;
|
|
96
|
+
getMetadata(input: SecretGetMetadataInput): Promise<SecretStoreResult<SecretMetadata>>;
|
|
97
|
+
create(input: SecretCreateInput): Promise<SecretStoreResult<SecretMetadata>>;
|
|
98
|
+
replace(input: SecretReplaceInput): Promise<SecretStoreResult<SecretMetadata>>;
|
|
99
|
+
remove(input: SecretRemoveInput): Promise<SecretStoreResult>;
|
|
100
|
+
resolve(input: SecretResolveInput): Promise<SecretStoreResult<SecretPayload>>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const SECRET_REF_SEGMENT_PATTERN = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
|
|
104
|
+
|
|
105
|
+
export function normalizeSecretRef(value: string): SecretStoreResult<SecretRef> {
|
|
106
|
+
const normalized = value
|
|
107
|
+
.trim()
|
|
108
|
+
.replace(/^\/+|\/+$/g, '')
|
|
109
|
+
.replace(/\/{2,}/g, '/');
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
normalized.length === 0 ||
|
|
113
|
+
normalized.length > 255 ||
|
|
114
|
+
normalized.split('/').some((segment) => !SECRET_REF_SEGMENT_PATTERN.test(segment))
|
|
115
|
+
) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: {
|
|
119
|
+
code: 'invalid_reference',
|
|
120
|
+
message:
|
|
121
|
+
'Secret reference must contain lowercase path segments using letters, numbers, dots, underscores, or hyphens.',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { ok: true, data: normalized };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function normalizeSecretScope(scope: SecretScope): SecretStoreResult<SecretScope> {
|
|
130
|
+
const projectId = scope.projectId.trim();
|
|
131
|
+
const environment = scope.environment.trim();
|
|
132
|
+
|
|
133
|
+
if (projectId.length === 0 || environment.length === 0) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: {
|
|
137
|
+
code: 'invalid_config',
|
|
138
|
+
message: 'Secret scope requires non-empty projectId and environment values.',
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { ok: true, data: { projectId, environment } };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function validateSecretPayload(payload: SecretPayload): SecretStoreResult<SecretPayload> {
|
|
147
|
+
const entries = Object.entries(payload);
|
|
148
|
+
|
|
149
|
+
if (entries.length === 0) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
error: {
|
|
153
|
+
code: 'invalid_payload',
|
|
154
|
+
message: 'Secret payload must contain at least one field.',
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const [field, value] of entries) {
|
|
160
|
+
if (field.trim().length === 0 || typeof value !== 'string' || value.length === 0) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
error: {
|
|
164
|
+
code: 'invalid_payload',
|
|
165
|
+
message: `Secret payload field ${field.trim().length === 0 ? '<empty>' : field} must contain a non-empty string value.`,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { ok: true, data: Object.freeze(Object.fromEntries(entries)) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const FORBIDDEN_INLINE_SECRET_FIELDS = [
|
|
175
|
+
'apiKey',
|
|
176
|
+
'clientSecret',
|
|
177
|
+
'databasePassword',
|
|
178
|
+
'privateKey',
|
|
179
|
+
'serviceRoleKey',
|
|
180
|
+
'token',
|
|
181
|
+
] as const;
|
|
182
|
+
|
|
183
|
+
export function findForbiddenInlineSecretFields(value: unknown): readonly string[] {
|
|
184
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return [];
|
|
185
|
+
|
|
186
|
+
const record = value as Record<string, unknown>;
|
|
187
|
+
return FORBIDDEN_INLINE_SECRET_FIELDS.filter((field) => field in record);
|
|
188
|
+
}
|