ignotum 0.0.7 → 0.0.9
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/README.md +89 -101
- package/dist/cli/bin.mjs +1119 -195
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-DtX8qPrq.js → api-DzcR7spt.js} +36 -13
- package/dist/runtime/api-DzcR7spt.js.map +1 -0
- package/dist/runtime/{api-Dp4J-xt-.d.ts → api-jCl8Hzry.d.ts} +7 -9
- package/dist/runtime/client.d.ts +12 -5
- package/dist/runtime/client.js +285 -60
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{descriptor-t6BOEGw9-C1rwYIlx.js → descriptor-C5VA9qRl-BKbdenuU.js} +38 -7
- package/dist/runtime/descriptor-C5VA9qRl-BKbdenuU.js.map +1 -0
- package/dist/runtime/file-C1abuMgd.js +173 -0
- package/dist/runtime/file-C1abuMgd.js.map +1 -0
- package/dist/runtime/{id-Btwac71X-DhnKYsjY.d.ts → id-Cs82tq9Q-Caqfx54f.d.ts} +51 -5
- package/dist/runtime/{index-B5KSOjGN.d.ts → index-D6VLTbDB.d.ts} +40 -21
- package/dist/runtime/internal/api.d.ts +2 -2
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +8 -7
- package/dist/runtime/internal/host.js +21 -10
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +1 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/{pagination-B1BzNkh8-BUSTbeSg.d.ts → pagination-Bt3l7QaC-D_zI5zsu.d.ts} +8 -4
- package/dist/runtime/pagination-DcIkTOFs.d.ts +1 -0
- package/dist/runtime/{schema-D9RmboaS.js → schema-B6PK_ZwV.js} +17 -3
- package/dist/runtime/schema-B6PK_ZwV.js.map +1 -0
- package/dist/runtime/server.d.ts +3 -3
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/sync-bMQq9tXx.d.ts +8 -0
- package/package.json +7 -5
- package/src/cli/agent-files.ts +52 -13
- package/src/cli/app-configuration.ts +4 -1
- package/src/cli/auth-client.ts +286 -0
- package/src/cli/build/server.ts +83 -6
- package/src/cli/command.ts +65 -2
- package/src/cli/control-client.ts +19 -6
- package/src/cli/new-app.ts +16 -0
- package/src/client/errors.ts +7 -10
- package/src/client/files.ts +153 -0
- package/src/client/hooks.ts +2 -15
- package/src/client/index.ts +7 -0
- package/src/client/sync.ts +158 -30
- package/src/dev-runtime/database.ts +69 -57
- package/src/dev-runtime/files.ts +337 -0
- package/src/dev-runtime/functions.ts +14 -6
- package/src/dev-runtime/migrations.ts +14 -0
- package/src/dev-runtime/sync.ts +134 -22
- package/src/internal/api.ts +16 -1
- package/src/server/index.ts +8 -1
- package/dist/runtime/api-DtX8qPrq.js.map +0 -1
- package/dist/runtime/descriptor-t6BOEGw9-C1rwYIlx.js.map +0 -1
- package/dist/runtime/id-D570vudg.js +0 -26
- package/dist/runtime/id-D570vudg.js.map +0 -1
- package/dist/runtime/pagination-BKPko9Hm.d.ts +0 -1
- package/dist/runtime/schema-D9RmboaS.js.map +0 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
import * as Versioned from "@ignotum/contracts/versioned";
|
|
5
|
+
import { createAuthClient } from "better-auth/client";
|
|
6
|
+
import { deviceAuthorizationClient, organizationClient } from "better-auth/client/plugins";
|
|
7
|
+
import {
|
|
8
|
+
Config,
|
|
9
|
+
Context,
|
|
10
|
+
Effect,
|
|
11
|
+
FileSystem,
|
|
12
|
+
Layer,
|
|
13
|
+
Option,
|
|
14
|
+
Path,
|
|
15
|
+
Redacted,
|
|
16
|
+
Schema,
|
|
17
|
+
Terminal,
|
|
18
|
+
} from "effect";
|
|
19
|
+
|
|
20
|
+
const CredentialV1 = Schema.Struct({
|
|
21
|
+
formatVersion: Schema.Literal(1),
|
|
22
|
+
authUrl: Schema.URLFromString,
|
|
23
|
+
token: Schema.String,
|
|
24
|
+
});
|
|
25
|
+
const CredentialThroughV1 = Versioned.initial(CredentialV1);
|
|
26
|
+
const Credential = CredentialThroughV1;
|
|
27
|
+
type Credential = typeof Credential.Type;
|
|
28
|
+
|
|
29
|
+
const AuthUrl = Schema.URL.check(
|
|
30
|
+
Schema.makeFilter(
|
|
31
|
+
(url) =>
|
|
32
|
+
url.protocol === "https:" ||
|
|
33
|
+
(url.protocol === "http:" && ["127.0.0.1", "::1", "localhost"].includes(url.hostname)),
|
|
34
|
+
{ message: "The auth URL must use HTTPS unless it targets localhost." },
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export class AuthCommandError extends Schema.TaggedError<AuthCommandError>()("AuthCommandError", {
|
|
39
|
+
cause: Schema.optional(Schema.Defect()),
|
|
40
|
+
message: Schema.String,
|
|
41
|
+
}) {}
|
|
42
|
+
|
|
43
|
+
const commandError = (message: string, cause?: unknown) =>
|
|
44
|
+
AuthCommandError.make(cause === undefined ? { message } : { cause, message });
|
|
45
|
+
|
|
46
|
+
interface StoredCredential {
|
|
47
|
+
readonly authUrl: URL;
|
|
48
|
+
readonly token: Redacted.Redacted<string>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface AuthCredentialStoreService {
|
|
52
|
+
readonly load: Effect.Effect<StoredCredential | undefined, AuthCommandError>;
|
|
53
|
+
readonly remove: Effect.Effect<boolean, AuthCommandError>;
|
|
54
|
+
readonly save: (credential: StoredCredential) => Effect.Effect<void, AuthCommandError>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class AuthCredentialStore extends Context.Service<
|
|
58
|
+
AuthCredentialStore,
|
|
59
|
+
AuthCredentialStoreService
|
|
60
|
+
>()("ignotum/cli/auth-client/AuthCredentialStore") {
|
|
61
|
+
static readonly layer = Layer.effect(
|
|
62
|
+
AuthCredentialStore,
|
|
63
|
+
Effect.gen(function* () {
|
|
64
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
65
|
+
const path = yield* Path.Path;
|
|
66
|
+
const configured = yield* Config.option(Config.string("IGNOTUM_CONFIG_DIR"));
|
|
67
|
+
const xdg = yield* Config.option(Config.string("XDG_CONFIG_HOME"));
|
|
68
|
+
const appData = yield* Config.option(Config.string("APPDATA"));
|
|
69
|
+
const configurationDirectory = Option.getOrElse(configured, () =>
|
|
70
|
+
Option.getOrElse(xdg, () =>
|
|
71
|
+
process.platform === "win32" && Option.isSome(appData)
|
|
72
|
+
? appData.value
|
|
73
|
+
: path.join(homedir(), ".config"),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
const directory = path.join(configurationDirectory, "ignotum");
|
|
77
|
+
const credentialPath = path.join(directory, "auth.json");
|
|
78
|
+
|
|
79
|
+
const load = Effect.fn("AuthCredentialStore.load")(function* () {
|
|
80
|
+
if (
|
|
81
|
+
!(yield* fileSystem
|
|
82
|
+
.exists(credentialPath)
|
|
83
|
+
.pipe(
|
|
84
|
+
Effect.mapError((cause) => commandError("Could not check the saved login.", cause)),
|
|
85
|
+
))
|
|
86
|
+
) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const text = yield* fileSystem
|
|
90
|
+
.readFileString(credentialPath)
|
|
91
|
+
.pipe(Effect.mapError((cause) => commandError("Could not read the saved login.", cause)));
|
|
92
|
+
const credential = yield* Schema.decodeEffect(Schema.fromJsonString(Credential))(text).pipe(
|
|
93
|
+
Effect.mapError((cause) => commandError("The saved login is invalid.", cause)),
|
|
94
|
+
);
|
|
95
|
+
return {
|
|
96
|
+
authUrl: credential.authUrl,
|
|
97
|
+
token: Redacted.make(credential.token),
|
|
98
|
+
} satisfies StoredCredential;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const save = Effect.fn("AuthCredentialStore.save")(function* (credential: StoredCredential) {
|
|
102
|
+
const value = {
|
|
103
|
+
formatVersion: 1,
|
|
104
|
+
authUrl: credential.authUrl,
|
|
105
|
+
token: Redacted.value(credential.token),
|
|
106
|
+
} satisfies Credential;
|
|
107
|
+
const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(Credential))(value).pipe(
|
|
108
|
+
Effect.mapError((cause) => commandError("Could not encode the login.", cause)),
|
|
109
|
+
);
|
|
110
|
+
yield* fileSystem
|
|
111
|
+
.makeDirectory(directory, { mode: 0o700, recursive: true })
|
|
112
|
+
.pipe(
|
|
113
|
+
Effect.mapError((cause) =>
|
|
114
|
+
commandError("Could not create the config directory.", cause),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
yield* Effect.scoped(
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
const temporaryPath = yield* fileSystem.makeTempFileScoped({
|
|
120
|
+
directory,
|
|
121
|
+
prefix: ".auth-",
|
|
122
|
+
suffix: ".json",
|
|
123
|
+
});
|
|
124
|
+
yield* fileSystem.writeFileString(temporaryPath, `${encoded}\n`, { mode: 0o600 });
|
|
125
|
+
yield* fileSystem.rename(temporaryPath, credentialPath);
|
|
126
|
+
}),
|
|
127
|
+
).pipe(Effect.mapError((cause) => commandError("Could not save the login.", cause)));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const remove = Effect.fn("AuthCredentialStore.remove")(function* () {
|
|
131
|
+
if (
|
|
132
|
+
!(yield* fileSystem
|
|
133
|
+
.exists(credentialPath)
|
|
134
|
+
.pipe(
|
|
135
|
+
Effect.mapError((cause) => commandError("Could not check the saved login.", cause)),
|
|
136
|
+
))
|
|
137
|
+
) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
yield* fileSystem
|
|
141
|
+
.remove(credentialPath)
|
|
142
|
+
.pipe(
|
|
143
|
+
Effect.mapError((cause) => commandError("Could not remove the saved login.", cause)),
|
|
144
|
+
);
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return AuthCredentialStore.of({ load: load(), remove: remove(), save });
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const makeClient = (authUrl: URL) =>
|
|
154
|
+
createAuthClient({
|
|
155
|
+
basePath: "/v1",
|
|
156
|
+
baseURL: authUrl.href,
|
|
157
|
+
plugins: [deviceAuthorizationClient(), organizationClient()],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const authorization = (token: Redacted.Redacted<string>) => ({
|
|
161
|
+
Authorization: `Bearer ${Redacted.value(token)}`,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const responseError = (error: { readonly error_description?: string; readonly message?: string }) =>
|
|
165
|
+
error.error_description ?? error.message ?? "Authentication failed.";
|
|
166
|
+
|
|
167
|
+
export const login = Effect.fn("Auth.login")(function* () {
|
|
168
|
+
const store = yield* AuthCredentialStore;
|
|
169
|
+
const terminal = yield* Terminal.Terminal;
|
|
170
|
+
const authUrl = yield* Config.schema(AuthUrl, "IGNOTUM_AUTH_URL").pipe(
|
|
171
|
+
Config.withDefault(new URL("https://auth.ignotum.cloud")),
|
|
172
|
+
Effect.mapError((cause) => commandError("The auth URL is invalid.", cause)),
|
|
173
|
+
);
|
|
174
|
+
const client = makeClient(authUrl);
|
|
175
|
+
const existing = yield* store.load;
|
|
176
|
+
if (existing !== undefined && existing.authUrl.href === authUrl.href) {
|
|
177
|
+
const result = yield* Effect.tryPromise({
|
|
178
|
+
try: () => client.getSession({ fetchOptions: { headers: authorization(existing.token) } }),
|
|
179
|
+
catch: (cause) => commandError("Could not check the saved login.", cause),
|
|
180
|
+
});
|
|
181
|
+
if (result.data?.user !== undefined) {
|
|
182
|
+
return {
|
|
183
|
+
email: result.data.user.email,
|
|
184
|
+
verificationUrl: undefined,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const started = yield* Effect.tryPromise({
|
|
190
|
+
try: () => client.device.code({ client_id: "ignotum-cli", scope: "openid profile email" }),
|
|
191
|
+
catch: (cause) => commandError("Could not start login.", cause),
|
|
192
|
+
});
|
|
193
|
+
if (started.data === null) {
|
|
194
|
+
return yield* commandError(responseError(started.error));
|
|
195
|
+
}
|
|
196
|
+
const url = started.data.verification_uri_complete ?? started.data.verification_uri;
|
|
197
|
+
yield* terminal.display(
|
|
198
|
+
`Open ${started.data.verification_uri}\nCode: ${started.data.user_code}\n\nWaiting for approval...\n`,
|
|
199
|
+
);
|
|
200
|
+
yield* Effect.tryPromise({
|
|
201
|
+
// @effect-diagnostics-next-line asyncFunction:off Dynamic import keeps browser-only code out of other CLI commands.
|
|
202
|
+
try: async () => (await import("open")).default(url),
|
|
203
|
+
catch: (cause) => commandError("Could not open the browser.", cause),
|
|
204
|
+
}).pipe(Effect.ignore);
|
|
205
|
+
|
|
206
|
+
let interval = started.data.interval ?? 5;
|
|
207
|
+
while (true) {
|
|
208
|
+
yield* Effect.sleep(`${interval} seconds`);
|
|
209
|
+
const polled = yield* Effect.tryPromise({
|
|
210
|
+
try: () =>
|
|
211
|
+
client.device.token({
|
|
212
|
+
client_id: "ignotum-cli",
|
|
213
|
+
device_code: started.data.device_code,
|
|
214
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
215
|
+
}),
|
|
216
|
+
catch: (cause) => commandError("Login polling failed.", cause),
|
|
217
|
+
});
|
|
218
|
+
if (polled.data?.access_token !== undefined) {
|
|
219
|
+
const token = Redacted.make(polled.data.access_token);
|
|
220
|
+
const session = yield* Effect.tryPromise({
|
|
221
|
+
try: () => client.getSession({ fetchOptions: { headers: authorization(token) } }),
|
|
222
|
+
catch: (cause) => commandError("Could not read the new session.", cause),
|
|
223
|
+
});
|
|
224
|
+
if (session.data?.user === undefined) {
|
|
225
|
+
return yield* commandError("The auth service returned an invalid session.");
|
|
226
|
+
}
|
|
227
|
+
yield* store.save({ authUrl, token });
|
|
228
|
+
return { email: session.data.user.email, verificationUrl: url };
|
|
229
|
+
}
|
|
230
|
+
switch (polled.error?.error) {
|
|
231
|
+
case "authorization_pending":
|
|
232
|
+
break;
|
|
233
|
+
case "slow_down":
|
|
234
|
+
interval += 5;
|
|
235
|
+
break;
|
|
236
|
+
case "access_denied":
|
|
237
|
+
return yield* commandError("Login was denied.");
|
|
238
|
+
case "expired_token":
|
|
239
|
+
return yield* commandError("The login code expired. Run login again.");
|
|
240
|
+
default:
|
|
241
|
+
return yield* commandError(
|
|
242
|
+
polled.error === null ? "Login failed." : responseError(polled.error),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
export const logout = Effect.fn("Auth.logout")(function* () {
|
|
249
|
+
const store = yield* AuthCredentialStore;
|
|
250
|
+
const credential = yield* store.load;
|
|
251
|
+
if (credential === undefined) return false;
|
|
252
|
+
const client = makeClient(credential.authUrl);
|
|
253
|
+
const result = yield* Effect.tryPromise({
|
|
254
|
+
try: () => client.signOut({ fetchOptions: { headers: authorization(credential.token) } }),
|
|
255
|
+
catch: (cause) => commandError("Could not revoke the session.", cause),
|
|
256
|
+
});
|
|
257
|
+
if (result.error !== null) {
|
|
258
|
+
return yield* commandError(responseError(result.error));
|
|
259
|
+
}
|
|
260
|
+
yield* store.remove;
|
|
261
|
+
return true;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
export const status = Effect.fn("Auth.status")(function* () {
|
|
265
|
+
const store = yield* AuthCredentialStore;
|
|
266
|
+
const credential = yield* store.load;
|
|
267
|
+
if (credential === undefined) return undefined;
|
|
268
|
+
const client = makeClient(credential.authUrl);
|
|
269
|
+
const session = yield* Effect.tryPromise({
|
|
270
|
+
try: () => client.getSession({ fetchOptions: { headers: authorization(credential.token) } }),
|
|
271
|
+
catch: (cause) => commandError("Could not check login status.", cause),
|
|
272
|
+
});
|
|
273
|
+
if (session.data?.user === undefined) return undefined;
|
|
274
|
+
const teams = yield* Effect.tryPromise({
|
|
275
|
+
try: () =>
|
|
276
|
+
client.organization.list({
|
|
277
|
+
fetchOptions: { headers: authorization(credential.token) },
|
|
278
|
+
}),
|
|
279
|
+
catch: (cause) => commandError("Could not load the team.", cause),
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
email: session.data.user.email,
|
|
283
|
+
name: session.data.user.name,
|
|
284
|
+
teamName: teams.data?.[0]?.name,
|
|
285
|
+
};
|
|
286
|
+
});
|
package/src/cli/build/server.ts
CHANGED
|
@@ -16,7 +16,15 @@ import {
|
|
|
16
16
|
} from "@ignotum/contracts/deployment";
|
|
17
17
|
import { FunctionAddress } from "@ignotum/contracts/runtime/sync";
|
|
18
18
|
import type { FunctionKind } from "@ignotum/contracts/runtime/functions";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
descriptorContainsFile,
|
|
21
|
+
descriptorFields,
|
|
22
|
+
getValueDescriptor,
|
|
23
|
+
isDefinedSchema,
|
|
24
|
+
ValueDescriptor,
|
|
25
|
+
type DefinedSchema,
|
|
26
|
+
type Tables,
|
|
27
|
+
} from "@ignotum/contracts/schema";
|
|
20
28
|
import {
|
|
21
29
|
artifactReference,
|
|
22
30
|
encodeCanonical,
|
|
@@ -41,17 +49,23 @@ const encodeJavaScriptString = Schema.encodeSync(Schema.fromJsonString(Schema.St
|
|
|
41
49
|
|
|
42
50
|
const DiscoveredServerFunction = Schema.Struct({
|
|
43
51
|
address: FunctionAddress,
|
|
52
|
+
args: Schema.optional(ValueDescriptor),
|
|
53
|
+
errors: Schema.optional(ValueDescriptor),
|
|
44
54
|
exportName: Schema.String,
|
|
45
55
|
kind: Schema.Literals(["Mutation", "Query"]),
|
|
46
56
|
moduleName: Schema.String,
|
|
47
57
|
modulePath: Schema.String,
|
|
58
|
+
returns: Schema.optional(ValueDescriptor),
|
|
48
59
|
});
|
|
49
60
|
interface DiscoveredServerFunction {
|
|
50
61
|
readonly address: FunctionAddress;
|
|
62
|
+
readonly args?: ValueDescriptor;
|
|
63
|
+
readonly errors?: ValueDescriptor;
|
|
51
64
|
readonly exportName: string;
|
|
52
65
|
readonly kind: FunctionKind;
|
|
53
66
|
readonly moduleName: string;
|
|
54
67
|
readonly modulePath: string;
|
|
68
|
+
readonly returns?: ValueDescriptor;
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
const ServerDiscoveryManifest = Schema.Struct({
|
|
@@ -179,13 +193,68 @@ const discoverModuleFunctions = Effect.fn("Deploy.discoverServerModule")(functio
|
|
|
179
193
|
continue;
|
|
180
194
|
}
|
|
181
195
|
|
|
182
|
-
|
|
196
|
+
const args =
|
|
197
|
+
inspected.definition.args === undefined
|
|
198
|
+
? undefined
|
|
199
|
+
: ({ type: "object", fields: descriptorFields(inspected.definition.args) } as const);
|
|
200
|
+
const returns =
|
|
201
|
+
inspected.definition.returns === undefined
|
|
202
|
+
? undefined
|
|
203
|
+
: getValueDescriptor(inspected.definition.returns);
|
|
204
|
+
const errors =
|
|
205
|
+
inspected.definition.errors === undefined
|
|
206
|
+
? undefined
|
|
207
|
+
: getValueDescriptor(inspected.definition.errors);
|
|
208
|
+
if (
|
|
209
|
+
(inspected.definition.returns !== undefined && returns === undefined) ||
|
|
210
|
+
(inspected.definition.errors !== undefined && errors === undefined)
|
|
211
|
+
) {
|
|
212
|
+
return yield* InvalidServerFunctionExport.make({
|
|
213
|
+
exportName,
|
|
214
|
+
message: `${moduleName}.${exportName} uses a validator that was not created by Ignotum values.`,
|
|
215
|
+
path: modulePath,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (
|
|
219
|
+
inspected.definition._tag === "Query" &&
|
|
220
|
+
args !== undefined &&
|
|
221
|
+
descriptorContainsFile(args)
|
|
222
|
+
) {
|
|
223
|
+
return yield* InvalidServerFunctionExport.make({
|
|
224
|
+
exportName,
|
|
225
|
+
message: `${moduleName}.${exportName} cannot accept files in query arguments.`,
|
|
226
|
+
path: modulePath,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (
|
|
230
|
+
inspected.definition._tag === "Mutation" &&
|
|
231
|
+
returns !== undefined &&
|
|
232
|
+
descriptorContainsFile(returns)
|
|
233
|
+
) {
|
|
234
|
+
return yield* InvalidServerFunctionExport.make({
|
|
235
|
+
exportName,
|
|
236
|
+
message: `${moduleName}.${exportName} cannot return files from a mutation.`,
|
|
237
|
+
path: modulePath,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (errors !== undefined && descriptorContainsFile(errors)) {
|
|
241
|
+
return yield* InvalidServerFunctionExport.make({
|
|
242
|
+
exportName,
|
|
243
|
+
message: `${moduleName}.${exportName} cannot include files in application errors.`,
|
|
244
|
+
path: modulePath,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const discovered = {
|
|
183
249
|
address: FunctionAddress.make(`api.${moduleName}.${exportName}`),
|
|
184
250
|
exportName,
|
|
185
251
|
kind: inspected.definition._tag,
|
|
186
252
|
moduleName,
|
|
187
253
|
modulePath,
|
|
188
|
-
}
|
|
254
|
+
};
|
|
255
|
+
const withArgs = args === undefined ? discovered : { ...discovered, args };
|
|
256
|
+
const withErrors = errors === undefined ? withArgs : { ...withArgs, errors };
|
|
257
|
+
functions.push(returns === undefined ? withErrors : { ...withErrors, returns });
|
|
189
258
|
}
|
|
190
259
|
|
|
191
260
|
return Array.sortWith(functions, (definition) => definition.address, String.Order);
|
|
@@ -319,6 +388,7 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
|
|
|
319
388
|
appDirectory,
|
|
320
389
|
nodeModulesDirectory(),
|
|
321
390
|
packageRoot,
|
|
391
|
+
path.join(packageRoot, "node_modules"),
|
|
322
392
|
...workspaceProbes,
|
|
323
393
|
].map((allowedPath) => `--allow-fs-read=${allowedPath}`);
|
|
324
394
|
const exitCode = yield* spawner.exitCode(
|
|
@@ -476,7 +546,7 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (
|
|
|
476
546
|
const bundlePath = normalizePath(path.join("server", relativeFile));
|
|
477
547
|
const sourceMapPath = normalizePath(path.join("server", relativeSourceMap));
|
|
478
548
|
|
|
479
|
-
|
|
549
|
+
const artifact = {
|
|
480
550
|
address: definition.address,
|
|
481
551
|
kind: definition.kind,
|
|
482
552
|
bundle: yield* artifactReference(bundlePath, yield* fileSystem.readFile(absoluteFile)),
|
|
@@ -484,7 +554,14 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (
|
|
|
484
554
|
sourceMapPath,
|
|
485
555
|
yield* fileSystem.readFile(absoluteSourceMap),
|
|
486
556
|
),
|
|
487
|
-
}
|
|
557
|
+
};
|
|
558
|
+
const withArgs =
|
|
559
|
+
definition.args === undefined ? artifact : { ...artifact, args: definition.args };
|
|
560
|
+
const withErrors =
|
|
561
|
+
definition.errors === undefined ? withArgs : { ...withArgs, errors: definition.errors };
|
|
562
|
+
return definition.returns === undefined
|
|
563
|
+
? (withErrors satisfies ServerFunctionArtifact)
|
|
564
|
+
: ({ ...withErrors, returns: definition.returns } satisfies ServerFunctionArtifact);
|
|
488
565
|
});
|
|
489
566
|
|
|
490
567
|
export const buildServer = Effect.fn("Deploy.buildServer")(function* (
|
|
@@ -507,7 +584,7 @@ export const buildServer = Effect.fn("Deploy.buildServer")(function* (
|
|
|
507
584
|
{ concurrency: 4 },
|
|
508
585
|
);
|
|
509
586
|
const manifest = {
|
|
510
|
-
formatVersion:
|
|
587
|
+
formatVersion: 2,
|
|
511
588
|
schema: yield* artifactReference(schemaSnapshotPath, snapshotBytes),
|
|
512
589
|
functions,
|
|
513
590
|
} satisfies ServerBuildManifest;
|
package/src/cli/command.ts
CHANGED
|
@@ -6,6 +6,12 @@ import { IdGenerator } from "@ignotum/shared/id";
|
|
|
6
6
|
|
|
7
7
|
import packageJson from "../../package.json" with { type: "json" };
|
|
8
8
|
import { generate } from "./codegen.js";
|
|
9
|
+
import {
|
|
10
|
+
AuthCredentialStore,
|
|
11
|
+
login as runLogin,
|
|
12
|
+
logout as runLogout,
|
|
13
|
+
status as readAuthStatus,
|
|
14
|
+
} from "./auth-client.js";
|
|
9
15
|
import { ControlClient } from "./control-client.js";
|
|
10
16
|
import { deploy as runDeploy } from "./deploy.js";
|
|
11
17
|
import { resetDevDatabase } from "../dev-runtime/dev-database.js";
|
|
@@ -15,6 +21,58 @@ import { installDependencies } from "./package-manager.js";
|
|
|
15
21
|
|
|
16
22
|
const DevPort = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65_535 }));
|
|
17
23
|
|
|
24
|
+
const authLogin = Command.make(
|
|
25
|
+
"login",
|
|
26
|
+
{},
|
|
27
|
+
Effect.fn("auth login")(function* () {
|
|
28
|
+
const terminal = yield* Terminal.Terminal;
|
|
29
|
+
const result = yield* runLogin().pipe(
|
|
30
|
+
Effect.mapError((error) =>
|
|
31
|
+
CliError.UserError.make({ cause: error, userMessage: error.message }),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
yield* terminal.display(`Signed in as ${result.email}.\n`);
|
|
35
|
+
}),
|
|
36
|
+
).pipe(Command.withDescription("Sign in to Ignotum with GitHub."));
|
|
37
|
+
|
|
38
|
+
const authLogout = Command.make(
|
|
39
|
+
"logout",
|
|
40
|
+
{},
|
|
41
|
+
Effect.fn("auth logout")(function* () {
|
|
42
|
+
const terminal = yield* Terminal.Terminal;
|
|
43
|
+
const removed = yield* runLogout().pipe(
|
|
44
|
+
Effect.mapError((error) =>
|
|
45
|
+
CliError.UserError.make({ cause: error, userMessage: error.message }),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
yield* terminal.display(removed ? "Signed out.\n" : "Not signed in.\n");
|
|
49
|
+
}),
|
|
50
|
+
).pipe(Command.withDescription("Revoke and remove the saved login."));
|
|
51
|
+
|
|
52
|
+
const authStatus = Command.make(
|
|
53
|
+
"status",
|
|
54
|
+
{},
|
|
55
|
+
Effect.fn("auth status")(function* () {
|
|
56
|
+
const terminal = yield* Terminal.Terminal;
|
|
57
|
+
const current = yield* readAuthStatus().pipe(
|
|
58
|
+
Effect.mapError((error) =>
|
|
59
|
+
CliError.UserError.make({ cause: error, userMessage: error.message }),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
if (current === undefined) {
|
|
63
|
+
yield* terminal.display("Not signed in.\n");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const team = current.teamName === undefined ? "" : `\nTeam: ${current.teamName}`;
|
|
67
|
+
yield* terminal.display(`Signed in as ${current.name} (${current.email}).${team}\n`);
|
|
68
|
+
}),
|
|
69
|
+
).pipe(Command.withDescription("Show the current Ignotum login."));
|
|
70
|
+
|
|
71
|
+
const auth = Command.make("auth").pipe(
|
|
72
|
+
Command.withDescription("Manage your Ignotum login."),
|
|
73
|
+
Command.withSubcommands([authLogin, authLogout, authStatus]),
|
|
74
|
+
);
|
|
75
|
+
|
|
18
76
|
const codegen = Command.make(
|
|
19
77
|
"codegen",
|
|
20
78
|
{},
|
|
@@ -172,7 +230,7 @@ const newApp = Command.make(
|
|
|
172
230
|
);
|
|
173
231
|
|
|
174
232
|
const ignotum = Command.make("ignotum").pipe(
|
|
175
|
-
Command.withSubcommands([newApp, install, codegen, dev, deploy]),
|
|
233
|
+
Command.withSubcommands([newApp, install, codegen, dev, deploy, auth]),
|
|
176
234
|
);
|
|
177
235
|
|
|
178
236
|
export const main = (): void => {
|
|
@@ -180,7 +238,12 @@ export const main = (): void => {
|
|
|
180
238
|
Command.run({ version: packageJson.version }),
|
|
181
239
|
// @effect-diagnostics-next-line strictEffectProvide:off
|
|
182
240
|
Effect.provide(
|
|
183
|
-
Layer.mergeAll(
|
|
241
|
+
Layer.mergeAll(
|
|
242
|
+
NodeServices.layer,
|
|
243
|
+
NodeHttpClient.layerUndici,
|
|
244
|
+
IdGenerator.layer,
|
|
245
|
+
AuthCredentialStore.layer.pipe(Layer.provide(NodeServices.layer)),
|
|
246
|
+
),
|
|
184
247
|
),
|
|
185
248
|
NodeRuntime.runMain,
|
|
186
249
|
);
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from "effect";
|
|
42
42
|
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
43
43
|
import type { HttpClientRequest as HttpRequest } from "effect/unstable/http/HttpClientRequest";
|
|
44
|
+
import { AuthCommandError, AuthCredentialStore } from "./auth-client.js";
|
|
44
45
|
|
|
45
46
|
export interface HostedControlConfig {
|
|
46
47
|
readonly apiUrl: URL;
|
|
@@ -69,11 +70,19 @@ export class HostedControlConfiguration extends Context.Service<
|
|
|
69
70
|
>()("ignotum/cli/control-client/HostedControlConfiguration") {
|
|
70
71
|
static readonly layer = Layer.effect(
|
|
71
72
|
HostedControlConfiguration,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
Effect.gen(function* () {
|
|
74
|
+
const credential = yield* (yield* AuthCredentialStore).load;
|
|
75
|
+
if (credential === undefined) {
|
|
76
|
+
return yield* AuthCommandError.make({
|
|
77
|
+
message: "Run 'npx ignotum auth login' first.",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
apiUrl: yield* Config.schema(HostedApiUrl, "IGNOTUM_API_URL").pipe(
|
|
82
|
+
Config.withDefault(new URL("https://api.ignotum.cloud")),
|
|
83
|
+
),
|
|
84
|
+
token: credential.token,
|
|
85
|
+
};
|
|
77
86
|
}),
|
|
78
87
|
);
|
|
79
88
|
}
|
|
@@ -133,7 +142,11 @@ export class ControlClient extends Context.Service<ControlClient, ControlClientS
|
|
|
133
142
|
"ignotum/cli/control-client/ControlClient",
|
|
134
143
|
) {
|
|
135
144
|
static get layer() {
|
|
136
|
-
return controlClientLayer.pipe(
|
|
145
|
+
return controlClientLayer.pipe(
|
|
146
|
+
Layer.provideMerge(
|
|
147
|
+
HostedControlConfiguration.layer.pipe(Layer.provide(AuthCredentialStore.layer)),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
package/src/cli/new-app.ts
CHANGED
|
@@ -207,6 +207,18 @@ Open <http://127.0.0.1:3210>.
|
|
|
207
207
|
The app generator creates \`_generated\`. The dev server checks those files before it starts
|
|
208
208
|
and updates them when the schema or server functions change.
|
|
209
209
|
|
|
210
|
+
## Deploy
|
|
211
|
+
|
|
212
|
+
Sign in, then choose a unique slug on the first deployment:
|
|
213
|
+
|
|
214
|
+
\`\`\`sh
|
|
215
|
+
npx ignotum auth login
|
|
216
|
+
npx ignotum deploy --app my-app
|
|
217
|
+
\`\`\`
|
|
218
|
+
|
|
219
|
+
The command prints the hosted URL and records the app link in \`.ignotum/app.json\`. Later
|
|
220
|
+
deployments use \`npx ignotum deploy\` without the \`--app\` flag.
|
|
221
|
+
|
|
210
222
|
## App files
|
|
211
223
|
|
|
212
224
|
- \`server/schema.ts\` defines the database tables.
|
|
@@ -216,6 +228,7 @@ and updates them when the schema or server functions change.
|
|
|
216
228
|
- Ignotum loads Tailwind CSS automatically. Custom CSS files are ordinary client modules.
|
|
217
229
|
- \`shared/utils.ts\` contains code shared across the app.
|
|
218
230
|
- \`_generated\` contains Ignotum's generated types and bindings. Do not edit it by hand.
|
|
231
|
+
- \`.agents/skills/ignotum\` contains the app skill and references used by coding agents.
|
|
219
232
|
|
|
220
233
|
Run the typechecker after a change:
|
|
221
234
|
|
|
@@ -224,6 +237,9 @@ npx ignotum codegen
|
|
|
224
237
|
npx tsc --noEmit
|
|
225
238
|
\`\`\`
|
|
226
239
|
|
|
240
|
+
The [Ignotum documentation](https://docs.ignotum.cloud) covers schema values, indexes, queries,
|
|
241
|
+
mutations, client hooks, deployment, guarantees, and hosted limits.
|
|
242
|
+
|
|
227
243
|
## Claude Code
|
|
228
244
|
|
|
229
245
|
If you use Claude Code, rename \`AGENTS.md\` to \`CLAUDE.md\` and \`.agents\` to \`.claude\` so it
|
package/src/client/errors.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** @effect-diagnostics globalConsole:skip-file */
|
|
2
2
|
import { Cause, Effect, ErrorReporter, Schema } from "effect";
|
|
3
3
|
|
|
4
|
-
import { FunctionAddress, Operation
|
|
4
|
+
import { ErrorCode, FunctionAddress, Operation } from "@ignotum/contracts/runtime/sync";
|
|
5
5
|
|
|
6
6
|
export class ConnectionUnavailable extends Schema.TaggedError<ConnectionUnavailable>()(
|
|
7
7
|
"ConnectionUnavailable",
|
|
@@ -36,21 +36,18 @@ export class InvalidServerMessage extends Schema.TaggedError<InvalidServerMessag
|
|
|
36
36
|
},
|
|
37
37
|
) {}
|
|
38
38
|
|
|
39
|
-
export class
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
operation: Schema.optional(Operation),
|
|
45
|
-
},
|
|
46
|
-
) {}
|
|
39
|
+
export class ServerError extends Schema.TaggedError<ServerError>()("ServerError", {
|
|
40
|
+
code: ErrorCode,
|
|
41
|
+
message: Schema.String,
|
|
42
|
+
operation: Schema.optional(Operation),
|
|
43
|
+
}) {}
|
|
47
44
|
|
|
48
45
|
export const ClientInfrastructureError = Schema.Union([
|
|
49
46
|
ConnectionUnavailable,
|
|
50
47
|
InvalidClientMessage,
|
|
51
48
|
InvalidMutationArguments,
|
|
52
49
|
InvalidServerMessage,
|
|
53
|
-
|
|
50
|
+
ServerError,
|
|
54
51
|
]);
|
|
55
52
|
export type ClientInfrastructureError = typeof ClientInfrastructureError.Type;
|
|
56
53
|
|