@knpkv/alchemy-console 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +118 -2
- package/dist/discovery.d.ts +12 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +39 -0
- package/dist/discovery.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/files.d.ts +9 -0
- package/dist/internal/files.d.ts.map +1 -0
- package/dist/internal/files.js +34 -0
- package/dist/internal/files.js.map +1 -0
- package/dist/local-state.d.ts +8 -0
- package/dist/local-state.d.ts.map +1 -0
- package/dist/local-state.js +53 -0
- package/dist/local-state.js.map +1 -0
- package/dist/profiles.d.ts +24 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +82 -0
- package/dist/profiles.js.map +1 -0
- package/dist/s3-live.d.ts +8 -0
- package/dist/s3-live.d.ts.map +1 -0
- package/dist/s3-live.js +111 -0
- package/dist/s3-live.js.map +1 -0
- package/dist/s3-reader.d.ts +46 -0
- package/dist/s3-reader.d.ts.map +1 -0
- package/dist/s3-reader.js +10 -0
- package/dist/s3-reader.js.map +1 -0
- package/dist/s3-state.d.ts +21 -0
- package/dist/s3-state.d.ts.map +1 -0
- package/dist/s3-state.js +129 -0
- package/dist/s3-state.js.map +1 -0
- package/dist/schema.d.ts +43 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +27 -0
- package/dist/schema.js.map +1 -0
- package/dist/sso-credentials.d.ts +17 -0
- package/dist/sso-credentials.d.ts.map +1 -0
- package/dist/sso-credentials.js +43 -0
- package/dist/sso-credentials.js.map +1 -0
- package/dist/state.d.ts +15 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +45 -0
- package/dist/state.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +61 -0
- package/dist/version.js.map +1 -0
- package/package.json +49 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 knpkv
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @knpkv/alchemy-console
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read-only data layer for a local Alchemy stack console. The package adds
|
|
4
|
+
project discovery, lockfile version lookup, local state metadata, and SSO/S3
|
|
5
|
+
readers. There is no executable or HTTP listener yet. Authenticated API and
|
|
6
|
+
design-backed Rly screens follow in separate PRs.
|
|
7
|
+
|
|
8
|
+
`discoverProjects(directory)` scans an explicit root for `alchemy.run.ts` without
|
|
9
|
+
importing or executing it. It skips `.git`, `node_modules`, `.alchemy`, `.direnv`,
|
|
10
|
+
`.cache`, and `dist`, deduplicates directory aliases, and rejects symlinks outside
|
|
11
|
+
the root. Directory errors fail the scan rather than hiding inaccessible projects.
|
|
12
|
+
|
|
13
|
+
`readAlchemyVersion(projectDirectory, searchRoot)` reads the nearest pnpm or Bun
|
|
14
|
+
text lockfile within the root. pnpm resolves the project's exact workspace
|
|
15
|
+
importer; Bun reads the resolved `alchemy` package tuple. Missing lockfiles or an
|
|
16
|
+
unresolved Alchemy version return `null`. Invalid lockfiles fail explicitly.
|
|
17
|
+
|
|
18
|
+
`readLocalState(directory, alchemyVersion)` reads
|
|
19
|
+
`.alchemy/state/<app>/<stage>/<encoded-fqn>.json` for beta.74 and beta.77. Other
|
|
20
|
+
versions, including unknown versions, fail with `unsupported-version`. Resource
|
|
21
|
+
metadata includes FQN, logical ID, type, status, and namespace parent. Action rows
|
|
22
|
+
are validated and excluded. Stack output and temporary files are ignored. Missing
|
|
23
|
+
state returns `available: false`; an existing empty stage returns an empty resource
|
|
24
|
+
list with `available: true`. JSON names must match the stored FQN encoding.
|
|
25
|
+
|
|
26
|
+
## SSO and S3
|
|
27
|
+
|
|
28
|
+
`discoverSsoProfiles(configFile)` reads the explicitly supplied AWS config file.
|
|
29
|
+
It resolves modern `sso_session` references and legacy inline SSO settings,
|
|
30
|
+
including the default profile. Static keys, credential processes, assumed-role
|
|
31
|
+
chains, and service sections are excluded. Missing config returns an empty list;
|
|
32
|
+
incomplete or conflicting SSO configuration fails with a safe tagged reason.
|
|
33
|
+
The executable will supply the local config path; this layer does not choose an
|
|
34
|
+
ambient profile or execute login commands.
|
|
35
|
+
|
|
36
|
+
`s3ReaderLayer(profile)` uses `ssoCredentialsLayer()` and an Effect `HttpClient`.
|
|
37
|
+
Credentials come exclusively from the selected SSO identity and cached local
|
|
38
|
+
session. There is no environment, static-key, instance-metadata, or process
|
|
39
|
+
credential fallback. The SDK may refresh an existing SSO session in its local
|
|
40
|
+
token cache. Credentials remain redacted in server memory and are used only for
|
|
41
|
+
request signing. The profile supplies the SSO portal region; listed bucket regions
|
|
42
|
+
route S3 requests. Object requests include the selected account as
|
|
43
|
+
`ExpectedBucketOwner`. S3 transport tracing is disabled so private request URLs
|
|
44
|
+
and signing headers cannot enter telemetry.
|
|
45
|
+
|
|
46
|
+
`discoverS3Buckets()` paginates the selected account's owned buckets and selects
|
|
47
|
+
names containing `alchemy-state`. A caller may supply another naming predicate
|
|
48
|
+
or explicit server-private bucket locators. Listing owned buckets cannot discover
|
|
49
|
+
every cross-account bucket that a role might be allowed to read. Denied access or
|
|
50
|
+
expired sessions fail explicitly; they do not produce an empty account catalog.
|
|
51
|
+
An API can report these failures per profile while continuing other profiles.
|
|
52
|
+
|
|
53
|
+
`discoverS3Stacks(bucket)` recognizes `<optional-prefix><app>/<stage>/<encoded-fqn>.json`
|
|
54
|
+
keys. `listS3Versions(bucket, prefix)` returns persisted object version coordinates
|
|
55
|
+
and deletion markers. Both S3 continuation markers are retained; repeated markers
|
|
56
|
+
fail. `readS3State(stack, alchemyVersion?)` reads each latest non-deleted resource
|
|
57
|
+
using its listed version ID. Stack outputs and temporary files are excluded.
|
|
58
|
+
`readS3Resource(stack, key, versionId)` reads one historical version after checking
|
|
59
|
+
that the key belongs to that stack. JSON names must match the resource FQN.
|
|
60
|
+
Latest deletion markers never resurrect an older resource. Null versions from
|
|
61
|
+
unversioned or suspended buckets are supported. Multiple resource reads are not
|
|
62
|
+
an atomic snapshot of a deployment.
|
|
63
|
+
|
|
64
|
+
Remote state alone does not prove the installed Alchemy version or last successful
|
|
65
|
+
deploy. Both remain `null` unless a matching lockfile supplies the version.
|
|
66
|
+
Known beta.74/beta.77 resource shapes are validated even when remote version is
|
|
67
|
+
unknown; an explicitly supplied unsupported version fails. The adapter imports
|
|
68
|
+
only `ListBuckets`, `ListObjectVersions`, and `GetObject`. It never builds Alchemy's
|
|
69
|
+
state-store layer, which can create or reconfigure buckets.
|
|
70
|
+
|
|
71
|
+
Read-only IAM access needs `s3:ListAllMyBuckets`, `s3:ListBucketVersions`, and
|
|
72
|
+
`s3:GetObjectVersion` for version-pinned reads. No write policy is required. See the AWS
|
|
73
|
+
[version listing](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectVersions.html)
|
|
74
|
+
and [object reading](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html)
|
|
75
|
+
references. No live AWS or Alchemy stack is used by the tests.
|
|
76
|
+
|
|
77
|
+
The flat resource list retains namespace parents, including parents without their
|
|
78
|
+
own resource row. A consumer can group these into a tree. Provider IDs, account,
|
|
79
|
+
region, console links, and last deploy are currently `null`. Local file timestamps
|
|
80
|
+
do not prove a successful deployment. Readers do not invent missing metadata.
|
|
81
|
+
|
|
82
|
+
## Private data and redaction
|
|
83
|
+
|
|
84
|
+
Discovered directories and entrypoints are server-private filesystem locators.
|
|
85
|
+
Keep them in server memory; do not persist them to browser storage or emit them
|
|
86
|
+
through HTTP, logs, telemetry, or public error messages. The normalized stack and
|
|
87
|
+
resource summaries may cross an authenticated owner API boundary. They must never
|
|
88
|
+
appear on unauthenticated routes or public telemetry.
|
|
89
|
+
|
|
90
|
+
Persisted Alchemy state can contain plaintext secret payloads inside
|
|
91
|
+
`__redacted__` marker objects. This package never invokes Alchemy's reviver or
|
|
92
|
+
decrypts anything. It discards props, attrs, old state, action inputs, and outputs.
|
|
93
|
+
Marker objects in metadata become `<redacted>`, including sibling fields. Parsing
|
|
94
|
+
errors include only a tagged reason, never raw contents or parser diagnostics.
|
|
95
|
+
Reads are capped at 8 MiB per file and directory listings at 50,000 entries.
|
|
96
|
+
|
|
97
|
+
SSO profiles retain server-private names, config paths, account/role selectors,
|
|
98
|
+
session names, start URLs, and routing regions. S3 bucket names, state prefixes,
|
|
99
|
+
object keys, version IDs, and pagination markers are also server-private. Their
|
|
100
|
+
persisted representation is AWS config or S3 object coordinates; console code
|
|
101
|
+
holds them in server memory only. They must not cross HTTP, browser storage,
|
|
102
|
+
logs, telemetry, or public errors. Only normalized `StackState` and
|
|
103
|
+
`ResourceSummary` fields may cross the authenticated owner API. Credential-bearing
|
|
104
|
+
`SsoCredentials` results remain solely inside the signing boundary. S3 reads have
|
|
105
|
+
the same 8 MiB payload cap and a 50,000-entry / 100-page listing cap.
|
|
106
|
+
|
|
107
|
+
Cloudflare profiles will also remain server-side. The UI will never apply or
|
|
108
|
+
destroy. Plan diffs remain on demand, with no scheduled execution.
|
|
109
|
+
|
|
110
|
+
## Format references
|
|
111
|
+
|
|
112
|
+
Synthetic test states follow the pinned upstream
|
|
113
|
+
[resource format](https://github.com/alchemy-run/alchemy/blob/v2.0.0-beta.77/packages/alchemy/src/State/ResourceState.ts),
|
|
114
|
+
[local layout](https://github.com/alchemy-run/alchemy/blob/v2.0.0-beta.77/packages/alchemy/src/State/LocalState.ts),
|
|
115
|
+
and [state encoding](https://github.com/alchemy-run/alchemy/blob/v2.0.0-beta.77/packages/alchemy/src/State/StateEncoding.ts).
|
|
116
|
+
No real stack, account, bucket, credential, or project path is included in fixtures.
|
|
117
|
+
|
|
118
|
+
Run `pnpm --filter @knpkv/alchemy-console test` for the focused fixture tests.
|
|
119
|
+
Build workspace dependencies before the package's `check` script.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
2
|
+
import { StateReadError } from "./schema.js";
|
|
3
|
+
/** Server-private paths. Retain only in memory; never emit to HTTP, browser storage, logs, or telemetry. */
|
|
4
|
+
export declare const Project: Schema.Struct<{
|
|
5
|
+
readonly directory: Schema.String;
|
|
6
|
+
readonly entrypoint: Schema.String;
|
|
7
|
+
}>;
|
|
8
|
+
export interface Project extends Schema.Schema.Type<typeof Project> {
|
|
9
|
+
}
|
|
10
|
+
/** Read directory metadata only. No stack imports, evaluation, subprocesses, or credential reads. */
|
|
11
|
+
export declare const discoverProjects: (directory: string) => Effect.Effect<Project[], StateReadError, FileSystem.FileSystem | Path.Path>;
|
|
12
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE5C,4GAA4G;AAC5G,eAAO,MAAM,OAAO;;;EAAyE,CAAA;AAC7F,MAAM,WAAW,OAAQ,SAAQ,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,OAAO,CAAC;CAAG;AAItE,qGAAqG;AACrG,eAAO,MAAM,gBAAgB,oGAyB3B,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
2
|
+
import { entries, stat, withinRoot } from "./internal/files.js";
|
|
3
|
+
import { StateReadError } from "./schema.js";
|
|
4
|
+
/** Server-private paths. Retain only in memory; never emit to HTTP, browser storage, logs, or telemetry. */
|
|
5
|
+
export const Project = Schema.Struct({ directory: Schema.String, entrypoint: Schema.String });
|
|
6
|
+
const excluded = new Set([".git", "node_modules", ".alchemy", ".direnv", ".cache", "dist"]);
|
|
7
|
+
/** Read directory metadata only. No stack imports, evaluation, subprocesses, or credential reads. */
|
|
8
|
+
export const discoverProjects = Effect.fn("AlchemyConsole.discoverProjects")(function* (directory) {
|
|
9
|
+
const fs = yield* FileSystem.FileSystem;
|
|
10
|
+
const path = yield* Path.Path;
|
|
11
|
+
const root = yield* fs.realPath(directory).pipe(Effect.mapError(() => new StateReadError({ reason: "io" })));
|
|
12
|
+
const pending = [root];
|
|
13
|
+
const visited = new Set();
|
|
14
|
+
const projects = [];
|
|
15
|
+
while (pending.length > 0) {
|
|
16
|
+
const candidate = pending.pop();
|
|
17
|
+
if (candidate === undefined)
|
|
18
|
+
break;
|
|
19
|
+
const current = yield* withinRoot(root, candidate);
|
|
20
|
+
if (visited.has(current))
|
|
21
|
+
continue;
|
|
22
|
+
visited.add(current);
|
|
23
|
+
if (visited.size > 50_000)
|
|
24
|
+
return yield* new StateReadError({ reason: "limit" });
|
|
25
|
+
for (const name of yield* entries(current)) {
|
|
26
|
+
if (excluded.has(name))
|
|
27
|
+
continue;
|
|
28
|
+
const file = yield* withinRoot(root, path.join(current, name));
|
|
29
|
+
const info = yield* stat(file);
|
|
30
|
+
if (info.type === "Directory")
|
|
31
|
+
pending.push(file);
|
|
32
|
+
else if (name === "alchemy.run.ts" && info.type === "File") {
|
|
33
|
+
projects.push(Project.make({ directory: current, entrypoint: file }));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return projects.sort((a, b) => a.directory.localeCompare(b.directory));
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { discoverProjects, Project } from "./discovery.js";
|
|
2
|
+
export { readLocalState } from "./local-state.js";
|
|
3
|
+
export { discoverSsoProfiles, SsoProfile, SsoProfileError } from "./profiles.js";
|
|
4
|
+
export { s3ReaderLayer } from "./s3-live.js";
|
|
5
|
+
export { S3Reader, S3ReadError } from "./s3-reader.js";
|
|
6
|
+
export type { S3Bucket, S3BucketPage, S3Version, S3VersionPage, VersionCursor } from "./s3-reader.js";
|
|
7
|
+
export { discoverS3Buckets, discoverS3Stacks, listS3Versions, readS3Resource, readS3State } from "./s3-state.js";
|
|
8
|
+
export type { S3StackLocator } from "./s3-state.js";
|
|
9
|
+
export { ResourceSummary, StackState, StateReadError } from "./schema.js";
|
|
10
|
+
export { SsoCredentials, ssoCredentialsLayer } from "./sso-credentials.js";
|
|
11
|
+
export type { SsoCredentialProviderFactory } from "./sso-credentials.js";
|
|
12
|
+
export { decodeResource } from "./state.js";
|
|
13
|
+
export { readAlchemyVersion } from "./version.js";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACtD,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACrG,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChH,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACzE,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1E,YAAY,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAA;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { discoverProjects, Project } from "./discovery.js";
|
|
2
|
+
export { readLocalState } from "./local-state.js";
|
|
3
|
+
export { discoverSsoProfiles, SsoProfile, SsoProfileError } from "./profiles.js";
|
|
4
|
+
export { s3ReaderLayer } from "./s3-live.js";
|
|
5
|
+
export { S3Reader, S3ReadError } from "./s3-reader.js";
|
|
6
|
+
export { discoverS3Buckets, discoverS3Stacks, listS3Versions, readS3Resource, readS3State } from "./s3-state.js";
|
|
7
|
+
export { ResourceSummary, StackState, StateReadError } from "./schema.js";
|
|
8
|
+
export { SsoCredentials, ssoCredentialsLayer } from "./sso-credentials.js";
|
|
9
|
+
export { decodeResource } from "./state.js";
|
|
10
|
+
export { readAlchemyVersion } from "./version.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEtD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhH,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACzE,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAE1E,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path } from "effect";
|
|
2
|
+
import { StateReadError } from "../schema.js";
|
|
3
|
+
export declare const readText: (file: string) => Effect.Effect<string, StateReadError, FileSystem.FileSystem>;
|
|
4
|
+
export declare const entries: (directory: string) => Effect.Effect<string[], StateReadError, FileSystem.FileSystem>;
|
|
5
|
+
/** Resolve before inspecting a candidate; symlinks may not escape the caller's explicit root. */
|
|
6
|
+
export declare const withinRoot: (root: string, candidate: string) => Effect.Effect<string, StateReadError, FileSystem.FileSystem | Path.Path>;
|
|
7
|
+
export declare const exists: (file: string) => Effect.Effect<boolean, StateReadError, FileSystem.FileSystem>;
|
|
8
|
+
export declare const stat: (file: string) => Effect.Effect<FileSystem.File.Info, StateReadError, FileSystem.FileSystem>;
|
|
9
|
+
//# sourceMappingURL=files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/internal/files.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE7C,eAAO,MAAM,QAAQ,gFAKnB,CAAA;AAEF,eAAO,MAAM,OAAO,uFAOlB,CAAA;AAEF,iGAAiG;AACjG,eAAO,MAAM,UAAU,+GASrB,CAAA;AAEF,eAAO,MAAM,MAAM,iFAGjB,CAAA;AAEF,eAAO,MAAM,IAAI,8FAGf,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { collectBoundedText } from "@knpkv/bounded-io";
|
|
2
|
+
import { Effect, FileSystem, Path } from "effect";
|
|
3
|
+
import { StateReadError } from "../schema.js";
|
|
4
|
+
export const readText = Effect.fn("AlchemyConsole.readText")(function* (file) {
|
|
5
|
+
const fs = yield* FileSystem.FileSystem;
|
|
6
|
+
return yield* collectBoundedText(fs.stream(file), 8 * 1024 * 1024).pipe(Effect.mapError((error) => new StateReadError({ reason: error._tag === "ByteLimitExceeded" ? "limit" : "io" })));
|
|
7
|
+
});
|
|
8
|
+
export const entries = Effect.fn("AlchemyConsole.entries")(function* (directory) {
|
|
9
|
+
const fs = yield* FileSystem.FileSystem;
|
|
10
|
+
const names = yield* fs.readDirectory(directory).pipe(Effect.mapError(() => new StateReadError({ reason: "io" })));
|
|
11
|
+
if (names.length > 50_000)
|
|
12
|
+
return yield* new StateReadError({ reason: "limit" });
|
|
13
|
+
return names.sort();
|
|
14
|
+
});
|
|
15
|
+
/** Resolve before inspecting a candidate; symlinks may not escape the caller's explicit root. */
|
|
16
|
+
export const withinRoot = Effect.fn("AlchemyConsole.withinRoot")(function* (root, candidate) {
|
|
17
|
+
const fs = yield* FileSystem.FileSystem;
|
|
18
|
+
const path = yield* Path.Path;
|
|
19
|
+
const real = yield* fs.realPath(candidate).pipe(Effect.mapError(() => new StateReadError({ reason: "io" })));
|
|
20
|
+
const relative = path.relative(root, real);
|
|
21
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
22
|
+
return yield* new StateReadError({ reason: "outside-root" });
|
|
23
|
+
}
|
|
24
|
+
return real;
|
|
25
|
+
});
|
|
26
|
+
export const exists = Effect.fn("AlchemyConsole.exists")(function* (file) {
|
|
27
|
+
const fs = yield* FileSystem.FileSystem;
|
|
28
|
+
return yield* fs.exists(file).pipe(Effect.mapError(() => new StateReadError({ reason: "io" })));
|
|
29
|
+
});
|
|
30
|
+
export const stat = Effect.fn("AlchemyConsole.stat")(function* (file) {
|
|
31
|
+
const fs = yield* FileSystem.FileSystem;
|
|
32
|
+
return yield* fs.stat(file).pipe(Effect.mapError(() => new StateReadError({ reason: "io" })));
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/internal/files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE7C,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC,yBAAyB,CAAC,CAAC,QAAQ,CAAC,EAAC,IAAY;IACjF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAA;IACvC,OAAO,KAAK,CAAC,CAAC,kBAAkB,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,IAAI,CACrE,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,KAAK,mBAAmB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAChH,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,EAAE,CAAC,wBAAwB,CAAC,CAAC,QAAQ,CAAC,EAAC,SAAiB;IACpF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAA;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,IAAI,CACnD,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAC5D,CAAA;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM;QAAE,OAAO,KAAK,CAAC,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;IAChF,OAAO,KAAK,CAAC,IAAI,EAAE,CAAA;AACrB,CAAC,CAAC,CAAA;AAEF,iGAAiG;AACjG,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC,2BAA2B,CAAC,CAAC,QAAQ,CAAC,EAAC,IAAY,EAAE,SAAiB;IACxG,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAA;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAA;IAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IAC5G,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAC1C,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3F,OAAO,KAAK,CAAC,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;IAC9D,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,uBAAuB,CAAC,CAAC,QAAQ,CAAC,EAAC,IAAY;IAC7E,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAA;IACvC,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;AACjG,CAAC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,qBAAqB,CAAC,CAAC,QAAQ,CAAC,EAAC,IAAY;IACzE,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAA;IACvC,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;AAC/F,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path } from "effect";
|
|
2
|
+
import { type StackState, StateReadError } from "./schema.js";
|
|
3
|
+
/** Reads .alchemy/state/<app>/<stage>/<encoded-fqn>.json. Missing state is distinct from an empty stage. */
|
|
4
|
+
export declare const readLocalState: (directory: string, alchemyVersion: string | null) => Effect.Effect<{
|
|
5
|
+
available: boolean;
|
|
6
|
+
stacks: StackState[];
|
|
7
|
+
}, StateReadError, FileSystem.FileSystem | Path.Path>;
|
|
8
|
+
//# sourceMappingURL=local-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-state.d.ts","sourceRoot":"","sources":["../src/local-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEjD,OAAO,EAAE,KAAK,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAG7D,4GAA4G;AAC5G,eAAO,MAAM,cAAc;;;qDA2CzB,CAAA"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path } from "effect";
|
|
2
|
+
import { entries, exists, readText, stat, withinRoot } from "./internal/files.js";
|
|
3
|
+
import { StateReadError } from "./schema.js";
|
|
4
|
+
import { decodeResource } from "./state.js";
|
|
5
|
+
/** Reads .alchemy/state/<app>/<stage>/<encoded-fqn>.json. Missing state is distinct from an empty stage. */
|
|
6
|
+
export const readLocalState = Effect.fn("AlchemyConsole.readLocalState")(function* (directory, alchemyVersion) {
|
|
7
|
+
if (alchemyVersion !== "2.0.0-beta.74" && alchemyVersion !== "2.0.0-beta.77") {
|
|
8
|
+
return yield* new StateReadError({ reason: "unsupported-version" });
|
|
9
|
+
}
|
|
10
|
+
const fs = yield* FileSystem.FileSystem;
|
|
11
|
+
const path = yield* Path.Path;
|
|
12
|
+
const root = yield* fs.realPath(directory).pipe(Effect.mapError(() => new StateReadError({ reason: "io" })));
|
|
13
|
+
const candidate = path.join(root, ".alchemy", "state");
|
|
14
|
+
if (!(yield* exists(candidate)))
|
|
15
|
+
return { available: false, stacks: [] };
|
|
16
|
+
const stateRoot = yield* withinRoot(root, candidate);
|
|
17
|
+
const stacks = [];
|
|
18
|
+
for (const app of yield* entries(stateRoot)) {
|
|
19
|
+
const appDir = yield* withinRoot(stateRoot, path.join(stateRoot, app));
|
|
20
|
+
if ((yield* stat(appDir)).type !== "Directory")
|
|
21
|
+
continue;
|
|
22
|
+
for (const stage of yield* entries(appDir)) {
|
|
23
|
+
const stageDir = yield* withinRoot(appDir, path.join(appDir, stage));
|
|
24
|
+
if ((yield* stat(stageDir)).type !== "Directory")
|
|
25
|
+
continue;
|
|
26
|
+
const resources = [];
|
|
27
|
+
for (const name of yield* entries(stageDir)) {
|
|
28
|
+
if (!name.endsWith(".json") || name === "__stack_output__.json")
|
|
29
|
+
continue;
|
|
30
|
+
const file = yield* withinRoot(stageDir, path.join(stageDir, name));
|
|
31
|
+
if ((yield* stat(file)).type !== "File")
|
|
32
|
+
return yield* new StateReadError({ reason: "invalid-state" });
|
|
33
|
+
const resource = yield* decodeResource(yield* readText(file));
|
|
34
|
+
if (resource === null)
|
|
35
|
+
continue;
|
|
36
|
+
if (name !== `${resource.fqn.replaceAll("/", "__")}.json`) {
|
|
37
|
+
return yield* new StateReadError({ reason: "invalid-state" });
|
|
38
|
+
}
|
|
39
|
+
resources.push(resource);
|
|
40
|
+
}
|
|
41
|
+
stacks.push({
|
|
42
|
+
app,
|
|
43
|
+
stage,
|
|
44
|
+
backend: "local",
|
|
45
|
+
alchemyVersion,
|
|
46
|
+
lastDeploy: null,
|
|
47
|
+
resources: resources.sort((a, b) => a.fqn.localeCompare(b.fqn))
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { available: true, stacks };
|
|
52
|
+
});
|
|
53
|
+
//# sourceMappingURL=local-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-state.js","sourceRoot":"","sources":["../src/local-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACjF,OAAO,EAAmB,cAAc,EAAE,MAAM,aAAa,CAAA;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE3C,4GAA4G;AAC5G,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,EAAE,CAAC,+BAA+B,CAAC,CAAC,QAAQ,CAAC,EAChF,SAAiB,EACjB,cAA6B;IAE7B,IAAI,cAAc,KAAK,eAAe,IAAI,cAAc,KAAK,eAAe,EAAE,CAAC;QAC7E,OAAO,KAAK,CAAC,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC,CAAA;IACrE,CAAC;IACD,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAA;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAA;IAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IAC5G,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;IACtD,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;IACxE,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IACpD,MAAM,MAAM,GAAsB,EAAE,CAAA;IACpC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAA;QACtE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW;YAAE,SAAQ;QACxD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;YACpE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW;gBAAE,SAAQ;YAC1D,MAAM,SAAS,GAAG,EAAE,CAAA;YACpB,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC5C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,KAAK,uBAAuB;oBAAE,SAAQ;gBACzE,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAA;gBACnE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM;oBAAE,OAAO,KAAK,CAAC,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAA;gBACtG,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;gBAC7D,IAAI,QAAQ,KAAK,IAAI;oBAAE,SAAQ;gBAC/B,IAAI,IAAI,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;oBAC1D,OAAO,KAAK,CAAC,CAAC,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAA;gBAC/D,CAAC;gBACD,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YAC1B,CAAC;YACD,MAAM,CAAC,IAAI,CAAC;gBACV,GAAG;gBACH,KAAK;gBACL,OAAO,EAAE,OAAO;gBAChB,cAAc;gBACd,UAAU,EAAE,IAAI;gBAChB,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;aAChE,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;AACpC,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
/** Server-private SSO configuration. Keep every field in memory, out of HTTP, browser storage, logs and telemetry. */
|
|
3
|
+
export declare const SsoProfile: Schema.Struct<{
|
|
4
|
+
readonly name: Schema.NonEmptyString;
|
|
5
|
+
readonly configFile: Schema.NonEmptyString;
|
|
6
|
+
readonly account: Schema.NonEmptyString;
|
|
7
|
+
readonly role: Schema.NonEmptyString;
|
|
8
|
+
readonly region: Schema.NonEmptyString;
|
|
9
|
+
readonly ssoRegion: Schema.NonEmptyString;
|
|
10
|
+
readonly startUrl: Schema.NonEmptyString;
|
|
11
|
+
readonly session: Schema.NullOr<Schema.NonEmptyString>;
|
|
12
|
+
}>;
|
|
13
|
+
export interface SsoProfile extends Schema.Schema.Type<typeof SsoProfile> {
|
|
14
|
+
}
|
|
15
|
+
declare const SsoProfileError_base: Schema.Class<SsoProfileError, Schema.TaggedStruct<"SsoProfileError", {
|
|
16
|
+
readonly reason: Schema.Literals<readonly ["io", "limit", "invalid-config"]>;
|
|
17
|
+
}>, import("effect/Cause").YieldableError>;
|
|
18
|
+
/** Safe reason only; SDK/parser diagnostics can contain private configuration. */
|
|
19
|
+
export declare class SsoProfileError extends SsoProfileError_base {
|
|
20
|
+
}
|
|
21
|
+
/** Read only the supplied AWS config; skip static/process/role profiles and resolve modern or legacy SSO sections. */
|
|
22
|
+
export declare const discoverSsoProfiles: (configFile: string) => Effect.Effect<SsoProfile[], SsoProfileError, import("effect/FileSystem").FileSystem>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAGvC,sHAAsH;AACtH,eAAO,MAAM,UAAU;;;;;;;;;EASrB,CAAA;AACF,MAAM,WAAW,UAAW,SAAQ,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,UAAU,CAAC;CAAG;;;;AAE5E,kFAAkF;AAClF,qBAAa,eAAgB,SAAQ,oBAEnC;CAAG;AA2BL,sHAAsH;AACtH,eAAO,MAAM,mBAAmB,8GAkC9B,CAAA"}
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { exists, readText } from "./internal/files.js";
|
|
3
|
+
/** Server-private SSO configuration. Keep every field in memory, out of HTTP, browser storage, logs and telemetry. */
|
|
4
|
+
export const SsoProfile = Schema.Struct({
|
|
5
|
+
name: Schema.NonEmptyString,
|
|
6
|
+
configFile: Schema.NonEmptyString,
|
|
7
|
+
account: Schema.NonEmptyString,
|
|
8
|
+
role: Schema.NonEmptyString,
|
|
9
|
+
region: Schema.NonEmptyString,
|
|
10
|
+
ssoRegion: Schema.NonEmptyString,
|
|
11
|
+
startUrl: Schema.NonEmptyString,
|
|
12
|
+
session: Schema.NullOr(Schema.NonEmptyString)
|
|
13
|
+
});
|
|
14
|
+
/** Safe reason only; SDK/parser diagnostics can contain private configuration. */
|
|
15
|
+
export class SsoProfileError extends Schema.TaggedError()("SsoProfileError", {
|
|
16
|
+
reason: Schema.Literals(["io", "limit", "invalid-config"])
|
|
17
|
+
}) {
|
|
18
|
+
}
|
|
19
|
+
/** Select only SSO fields; static keys and commands are discarded while parsing. */
|
|
20
|
+
const parseSections = Effect.fnUntraced(function* (text) {
|
|
21
|
+
const sections = new Map();
|
|
22
|
+
let fields;
|
|
23
|
+
for (const line of text.split(/\r?\n/)) {
|
|
24
|
+
const trimmed = line.split(/(^|\s)[;#]/)[0]?.trim() ?? "";
|
|
25
|
+
if (trimmed.length === 0)
|
|
26
|
+
continue;
|
|
27
|
+
if (trimmed.startsWith("[")) {
|
|
28
|
+
const match = /^\[([^\]]+)\]$/.exec(trimmed);
|
|
29
|
+
if (match?.[1] === undefined || match[1].length === 0) {
|
|
30
|
+
return yield* new SsoProfileError({ reason: "invalid-config" });
|
|
31
|
+
}
|
|
32
|
+
const name = match[1].trim().replace(/^(profile|sso-session)\s+["']?([^"']+)["']?$/, "$1.$2");
|
|
33
|
+
fields = sections.get(name) ?? new Map();
|
|
34
|
+
sections.set(name, fields);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const equals = trimmed.indexOf("=");
|
|
38
|
+
if (fields === undefined || equals <= 0)
|
|
39
|
+
continue;
|
|
40
|
+
const key = trimmed.slice(0, equals).trim();
|
|
41
|
+
if (key === "region" || key.startsWith("sso_"))
|
|
42
|
+
fields.set(key, trimmed.slice(equals + 1).trim());
|
|
43
|
+
}
|
|
44
|
+
return new Map([...sections].map(([name, fields]) => [name, Object.fromEntries(fields)]));
|
|
45
|
+
});
|
|
46
|
+
/** Read only the supplied AWS config; skip static/process/role profiles and resolve modern or legacy SSO sections. */
|
|
47
|
+
export const discoverSsoProfiles = Effect.fn("AlchemyConsole.discoverSsoProfiles")(function* (configFile) {
|
|
48
|
+
const present = yield* exists(configFile).pipe(Effect.mapError(() => new SsoProfileError({ reason: "io" })));
|
|
49
|
+
if (!present)
|
|
50
|
+
return [];
|
|
51
|
+
const text = yield* readText(configFile).pipe(Effect.mapError((error) => new SsoProfileError({ reason: error.reason === "limit" ? "limit" : "io" })));
|
|
52
|
+
const ini = yield* parseSections(text);
|
|
53
|
+
const profiles = [];
|
|
54
|
+
for (const [section, fields] of ini) {
|
|
55
|
+
if (section !== "default" && !section.startsWith("profile."))
|
|
56
|
+
continue;
|
|
57
|
+
if (!Object.keys(fields).some((key) => key.startsWith("sso_")))
|
|
58
|
+
continue;
|
|
59
|
+
const session = fields.sso_session === undefined ? undefined : ini.get(`sso-session.${fields.sso_session}`);
|
|
60
|
+
if (fields.sso_session !== undefined && session === undefined) {
|
|
61
|
+
return yield* new SsoProfileError({ reason: "invalid-config" });
|
|
62
|
+
}
|
|
63
|
+
const ssoRegion = session?.sso_region ?? fields.sso_region;
|
|
64
|
+
const startUrl = session?.sso_start_url ?? fields.sso_start_url;
|
|
65
|
+
if ((fields.sso_region !== undefined && fields.sso_region !== ssoRegion) ||
|
|
66
|
+
(fields.sso_start_url !== undefined && fields.sso_start_url !== startUrl))
|
|
67
|
+
return yield* new SsoProfileError({ reason: "invalid-config" });
|
|
68
|
+
const profile = yield* Schema.decodeUnknownEffect(SsoProfile)({
|
|
69
|
+
name: section === "default" ? section : section.slice("profile.".length),
|
|
70
|
+
configFile,
|
|
71
|
+
account: fields.sso_account_id,
|
|
72
|
+
role: fields.sso_role_name,
|
|
73
|
+
region: fields.region ?? ssoRegion,
|
|
74
|
+
ssoRegion,
|
|
75
|
+
startUrl,
|
|
76
|
+
session: fields.sso_session ?? null
|
|
77
|
+
}).pipe(Effect.mapError(() => new SsoProfileError({ reason: "invalid-config" })));
|
|
78
|
+
profiles.push(profile);
|
|
79
|
+
}
|
|
80
|
+
return profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
81
|
+
});
|
|
82
|
+
//# sourceMappingURL=profiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.js","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
3
|
+
import type { SsoProfile } from "./profiles.js";
|
|
4
|
+
import { S3Reader } from "./s3-reader.js";
|
|
5
|
+
import { SsoCredentials } from "./sso-credentials.js";
|
|
6
|
+
/** Independent read-only adapter. ExpectedBucketOwner binds requests to the selected SSO account. */
|
|
7
|
+
export declare const s3ReaderLayer: (profile: SsoProfile) => Layer.Layer<S3Reader, never, HttpClient.HttpClient | SsoCredentials>;
|
|
8
|
+
//# sourceMappingURL=s3-live.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-live.d.ts","sourceRoot":"","sources":["../src/s3-live.ts"],"names":[],"mappings":"AAcA,OAAO,EAAU,KAAK,EAAa,MAAM,QAAQ,CAAA;AACjD,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC/C,OAAO,EAAiB,QAAQ,EAAmD,MAAM,gBAAgB,CAAA;AACzG,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAsBrD,qGAAqG;AACrG,eAAO,MAAM,aAAa,YAAa,UAAU,yEAqH9C,CAAA"}
|
package/dist/s3-live.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Credentials } from "@distilled.cloud/aws/Credentials";
|
|
2
|
+
import { Region } from "@distilled.cloud/aws/Region";
|
|
3
|
+
import { getObject, listBuckets, listObjectVersions } from "@distilled.cloud/aws/s3";
|
|
4
|
+
import { collectBoundedText } from "@knpkv/bounded-io";
|
|
5
|
+
import { Effect, Layer, Predicate } from "effect";
|
|
6
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
7
|
+
import { S3Reader, S3ReadError } from "./s3-reader.js";
|
|
8
|
+
import { SsoCredentials } from "./sso-credentials.js";
|
|
9
|
+
import { decodeResource } from "./state.js";
|
|
10
|
+
const providerError = (error) => {
|
|
11
|
+
if (error._tag === "S3ReadError")
|
|
12
|
+
return error;
|
|
13
|
+
const tag = error._tag === "UnknownAwsError" ? error.errorTag : error._tag;
|
|
14
|
+
if (tag === "AccessDenied" || tag === "AccessDeniedException") {
|
|
15
|
+
return new S3ReadError({ reason: "denied" });
|
|
16
|
+
}
|
|
17
|
+
if (["NoSuchKey", "NoSuchVersion", "NoSuchBucket"].includes(tag))
|
|
18
|
+
return new S3ReadError({ reason: "not-found" });
|
|
19
|
+
if (["ExpiredToken", "ExpiredTokenException", "InvalidToken", "InvalidAccessKeyId", "SignatureDoesNotMatch"].includes(tag)) {
|
|
20
|
+
return new S3ReadError({ reason: "authentication" });
|
|
21
|
+
}
|
|
22
|
+
return new S3ReadError({ reason: "provider" });
|
|
23
|
+
};
|
|
24
|
+
/** Independent read-only adapter. ExpectedBucketOwner binds requests to the selected SSO account. */
|
|
25
|
+
export const s3ReaderLayer = (profile) => Layer.effect(S3Reader, Effect.gen(function* () {
|
|
26
|
+
const client = yield* HttpClient.HttpClient;
|
|
27
|
+
const sso = yield* SsoCredentials;
|
|
28
|
+
const call = Effect.fn("AlchemyConsole.callS3")(function* (effect, region) {
|
|
29
|
+
const credentials = yield* sso.resolve(profile);
|
|
30
|
+
return yield* effect.pipe(Effect.provideService(Credentials, Effect.succeed(credentials)), Effect.provideService(Region, Effect.succeed(region)), Effect.provideService(HttpClient.HttpClient, client),
|
|
31
|
+
// S3 request URLs contain server-private bucket, key and version locators.
|
|
32
|
+
Effect.provideService(HttpClient.TracerDisabledWhen, () => true), Effect.timeoutOrElse({
|
|
33
|
+
duration: "30 seconds",
|
|
34
|
+
orElse: () => Effect.fail(new S3ReadError({ reason: "provider" }))
|
|
35
|
+
}), Effect.mapError(providerError));
|
|
36
|
+
});
|
|
37
|
+
return S3Reader.of({
|
|
38
|
+
buckets: Effect.fn("AlchemyConsole.listS3Buckets")(function* (cursor) {
|
|
39
|
+
const request = { MaxBuckets: 1000 };
|
|
40
|
+
if (cursor !== null)
|
|
41
|
+
request.ContinuationToken = cursor;
|
|
42
|
+
const page = yield* call(listBuckets(request), profile.region);
|
|
43
|
+
const buckets = [];
|
|
44
|
+
for (const bucket of page.Buckets ?? []) {
|
|
45
|
+
if (bucket.Name === undefined || bucket.Name.length === 0 || bucket.BucketRegion === undefined ||
|
|
46
|
+
bucket.BucketRegion.length === 0)
|
|
47
|
+
return yield* new S3ReadError({ reason: "invalid-response" });
|
|
48
|
+
buckets.push({ name: bucket.Name, region: bucket.BucketRegion });
|
|
49
|
+
}
|
|
50
|
+
return { buckets, next: page.ContinuationToken ?? null };
|
|
51
|
+
}),
|
|
52
|
+
versions: Effect.fn("AlchemyConsole.listS3Versions")(function* (bucket, prefix, cursor) {
|
|
53
|
+
const request = {
|
|
54
|
+
Bucket: bucket.name,
|
|
55
|
+
ExpectedBucketOwner: profile.account,
|
|
56
|
+
Prefix: prefix,
|
|
57
|
+
MaxKeys: 1000
|
|
58
|
+
};
|
|
59
|
+
if (cursor !== null) {
|
|
60
|
+
request.KeyMarker = cursor.key;
|
|
61
|
+
if (cursor.versionId !== null)
|
|
62
|
+
request.VersionIdMarker = cursor.versionId;
|
|
63
|
+
}
|
|
64
|
+
const page = yield* call(listObjectVersions(request), bucket.region);
|
|
65
|
+
const versions = [];
|
|
66
|
+
const groups = [
|
|
67
|
+
{ rows: page.Versions ?? [], deleted: false },
|
|
68
|
+
{ rows: page.DeleteMarkers ?? [], deleted: true }
|
|
69
|
+
];
|
|
70
|
+
for (const { deleted, rows } of groups) {
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
if (row.Key === undefined || row.Key.length === 0 || row.VersionId === undefined ||
|
|
73
|
+
row.VersionId.length === 0 || row.IsLatest === undefined) {
|
|
74
|
+
return yield* new S3ReadError({ reason: "invalid-response" });
|
|
75
|
+
}
|
|
76
|
+
versions.push({ key: row.Key, versionId: row.VersionId, latest: row.IsLatest, deleted });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (page.IsTruncated === undefined ||
|
|
80
|
+
(page.IsTruncated && (page.NextKeyMarker === undefined || page.NextKeyMarker.length === 0))) {
|
|
81
|
+
return yield* new S3ReadError({ reason: "invalid-response" });
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
versions,
|
|
85
|
+
next: page.IsTruncated === true ?
|
|
86
|
+
{
|
|
87
|
+
key: page.NextKeyMarker ?? "",
|
|
88
|
+
versionId: page.NextVersionIdMarker ?? null
|
|
89
|
+
} :
|
|
90
|
+
null
|
|
91
|
+
};
|
|
92
|
+
}),
|
|
93
|
+
resource: Effect.fn("AlchemyConsole.getS3Resource")(function* (bucket, key, versionId) {
|
|
94
|
+
const result = yield* call(getObject({
|
|
95
|
+
Bucket: bucket.name,
|
|
96
|
+
ExpectedBucketOwner: profile.account,
|
|
97
|
+
Key: key,
|
|
98
|
+
VersionId: versionId
|
|
99
|
+
}), bucket.region);
|
|
100
|
+
if ((result.VersionId ?? "null") !== versionId || result.DeleteMarker === true || result.Body === undefined) {
|
|
101
|
+
return yield* new S3ReadError({ reason: "invalid-response" });
|
|
102
|
+
}
|
|
103
|
+
const text = yield* collectBoundedText(result.Body, 8 * 1024 * 1024).pipe(Effect.mapError((error) => new S3ReadError({ reason: Predicate.isTagged(error, "ByteLimitExceeded") ? "limit" : "provider" })), Effect.timeoutOrElse({
|
|
104
|
+
duration: "30 seconds",
|
|
105
|
+
orElse: () => Effect.fail(new S3ReadError({ reason: "provider" }))
|
|
106
|
+
}));
|
|
107
|
+
return yield* decodeResource(text);
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
}));
|
|
111
|
+
//# sourceMappingURL=s3-live.js.map
|