@ts-cloud/core 0.7.119 → 0.7.121

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.
@@ -57,6 +57,13 @@ export interface FindComputeTargetsOptions {
57
57
  slug: string;
58
58
  environment: EnvironmentType;
59
59
  role?: string;
60
+ /**
61
+ * Region to search, for drivers that have one. Defaults to the driver's own.
62
+ * A driver method that resolved a region from config passes it here, so the
63
+ * lookup and whatever is done with the results cannot disagree about where
64
+ * the instances are.
65
+ */
66
+ region?: string;
60
67
  /**
61
68
  * Project stack name (`resolveProjectStackName(config, environment)`), used
62
69
  * by drivers that can pin targets from local state when label/tag scans
package/dist/index.js CHANGED
@@ -10647,11 +10647,45 @@ function normalizeHomeDirectory(value, username) {
10647
10647
  throw new Error(`sftp: invalid homeDirectory for user ${username}`);
10648
10648
  return path;
10649
10649
  }
10650
+ function resolveStorage(options) {
10651
+ const storage = options.storage;
10652
+ if (storage?.type === "efs") {
10653
+ const fileSystemId = "fileSystemId" in storage ? storage.fileSystemId : undefined;
10654
+ if (!fileSystemId || typeof fileSystemId === "string" && !fileSystemId.trim())
10655
+ throw new Error("sftp: EFS storage requires fileSystemId, or a fileSystem name defined in infrastructure.fileSystem");
10656
+ return { type: "efs", fileSystemId, posixProfile: storage.posixProfile };
10657
+ }
10658
+ const bucket = (storage?.type === "s3" ? storage.bucket ?? options.bucket : options.bucket)?.trim();
10659
+ if (!bucket)
10660
+ throw new Error("sftp: S3 storage requires bucket, or a storageBucket name defined in infrastructure.storage");
10661
+ return { type: "s3", bucket };
10662
+ }
10663
+ function posixProfileFor(username, user, fallback) {
10664
+ const profile = user.posixProfile ?? fallback;
10665
+ if (!profile)
10666
+ throw new Error(`sftp: user ${username} requires a posixProfile (uid/gid) on an EFS-backed server`);
10667
+ for (const id of [profile.uid, profile.gid, ...profile.secondaryGids ?? []]) {
10668
+ if (!Number.isInteger(id) || id < 0 || id > 4294967295)
10669
+ throw new Error(`sftp: user ${username} has an invalid posixProfile id ${id}`);
10670
+ }
10671
+ return {
10672
+ Uid: profile.uid,
10673
+ Gid: profile.gid,
10674
+ ...profile.secondaryGids?.length ? { SecondaryGids: profile.secondaryGids } : {}
10675
+ };
10676
+ }
10677
+ function efsHomeDirectory(fileSystemId, home) {
10678
+ return typeof fileSystemId === "string" ? `/${fileSystemId}/${home}` : { "Fn::Sub": [`/\${FileSystemId}/${home}`, { FileSystemId: fileSystemId }] };
10679
+ }
10680
+ function efsFileSystemArn(fileSystemId) {
10681
+ const template = "arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/${FileSystemId}";
10682
+ return typeof fileSystemId === "string" ? { "Fn::Sub": template.replace("${FileSystemId}", fileSystemId) } : { "Fn::Sub": [template, { FileSystemId: fileSystemId }] };
10683
+ }
10650
10684
 
10651
10685
  class Sftp {
10652
10686
  static create(options) {
10653
- if (!options.bucket.trim())
10654
- throw new Error("sftp: bucket is required");
10687
+ const storage = resolveStorage(options);
10688
+ const domain = storage.type === "efs" ? "EFS" : "S3";
10655
10689
  const endpointType = options.endpointType ?? "PUBLIC";
10656
10690
  if (endpointType === "VPC" && (!options.endpointDetails?.vpcId || !options.endpointDetails.subnetIds.length))
10657
10691
  throw new Error("sftp: VPC endpoints require endpointDetails.vpcId and at least one subnet");
@@ -10697,7 +10731,7 @@ class Sftp {
10697
10731
  resources[serverLogicalId] = {
10698
10732
  Type: "AWS::Transfer::Server",
10699
10733
  Properties: {
10700
- Domain: "S3",
10734
+ Domain: domain,
10701
10735
  EndpointType: endpointType,
10702
10736
  IdentityProviderType: "SERVICE_MANAGED",
10703
10737
  Protocols: ["SFTP"],
@@ -10729,7 +10763,35 @@ class Sftp {
10729
10763
  let roleArn = user.roleArn;
10730
10764
  if (!roleArn) {
10731
10765
  const roleLogicalId = `${prefix}${userPart}Role`;
10732
- const bucketArn = `arn:aws:s3:::${options.bucket}`;
10766
+ let statements;
10767
+ if (storage.type === "efs") {
10768
+ statements = [
10769
+ {
10770
+ Effect: "Allow",
10771
+ Action: [
10772
+ "elasticfilesystem:ClientMount",
10773
+ "elasticfilesystem:ClientWrite",
10774
+ "elasticfilesystem:DescribeMountTargets"
10775
+ ],
10776
+ Resource: efsFileSystemArn(storage.fileSystemId)
10777
+ }
10778
+ ];
10779
+ } else {
10780
+ const bucketArn = `arn:aws:s3:::${storage.bucket}`;
10781
+ statements = [
10782
+ {
10783
+ Effect: "Allow",
10784
+ Action: ["s3:ListBucket", "s3:GetBucketLocation"],
10785
+ Resource: bucketArn,
10786
+ Condition: { StringLike: { "s3:prefix": [home, `${home}/*`] } }
10787
+ },
10788
+ {
10789
+ Effect: "Allow",
10790
+ Action: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:GetObjectVersion"],
10791
+ Resource: `${bucketArn}/${home}/*`
10792
+ }
10793
+ ];
10794
+ }
10733
10795
  resources[roleLogicalId] = {
10734
10796
  Type: "AWS::IAM::Role",
10735
10797
  Properties: {
@@ -10742,22 +10804,7 @@ class Sftp {
10742
10804
  Policies: [
10743
10805
  {
10744
10806
  PolicyName: "SftpHomeDirectory",
10745
- PolicyDocument: {
10746
- Version: "2012-10-17",
10747
- Statement: [
10748
- {
10749
- Effect: "Allow",
10750
- Action: ["s3:ListBucket", "s3:GetBucketLocation"],
10751
- Resource: bucketArn,
10752
- Condition: { StringLike: { "s3:prefix": [home, `${home}/*`] } }
10753
- },
10754
- {
10755
- Effect: "Allow",
10756
- Action: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:GetObjectVersion"],
10757
- Resource: `${bucketArn}/${home}/*`
10758
- }
10759
- ]
10760
- }
10807
+ PolicyDocument: { Version: "2012-10-17", Statement: statements }
10761
10808
  }
10762
10809
  ]
10763
10810
  }
@@ -10772,12 +10819,13 @@ class Sftp {
10772
10819
  UserName: username,
10773
10820
  Role: roleArn,
10774
10821
  HomeDirectoryType: "PATH",
10775
- HomeDirectory: `/${options.bucket}/${home}`,
10822
+ HomeDirectory: storage.type === "efs" ? efsHomeDirectory(storage.fileSystemId, home) : `/${storage.bucket}/${home}`,
10823
+ ...storage.type === "efs" ? { PosixProfile: posixProfileFor(username, user, storage.posixProfile) } : {},
10776
10824
  SshPublicKeys: user.sshPublicKeys
10777
10825
  }
10778
10826
  };
10779
10827
  }
10780
- return { resources, serverLogicalId };
10828
+ return { resources, serverLogicalId, domain };
10781
10829
  }
10782
10830
  }
10783
10831
  // src/modules/ai.ts
@@ -1,12 +1,33 @@
1
- import type { EnvironmentType, SftpConfig } from '../types';
1
+ import type { EnvironmentType, SftpConfig, SftpPosixProfile } from '../types';
2
2
  export interface SftpResources {
3
3
  resources: Record<string, any>;
4
4
  serverLogicalId: string;
5
+ /** Storage backend the server was built with. */
6
+ domain: 'S3' | 'EFS';
7
+ }
8
+ /**
9
+ * An EFS file system ID: either a literal `fs-…` or a CloudFormation intrinsic
10
+ * (`{ Ref: '…' }`) pointing at a file system created in the same stack.
11
+ */
12
+ export type SftpFileSystemRef = string | Record<string, any>;
13
+ /**
14
+ * Storage the module resolved down to a concrete backend. The generator turns
15
+ * `storageBucket`/`fileSystem` references into these before calling `create`.
16
+ */
17
+ export type ResolvedSftpStorage = {
18
+ type: 's3';
19
+ bucket: string;
20
+ } | {
21
+ type: 'efs';
22
+ fileSystemId: SftpFileSystemRef;
23
+ posixProfile?: SftpPosixProfile;
24
+ };
25
+ export interface SftpCreateOptions extends Omit<SftpConfig, 'storage'> {
26
+ slug: string;
27
+ environment: EnvironmentType;
28
+ storage?: SftpConfig['storage'] | ResolvedSftpStorage;
5
29
  }
6
30
  /** Build an AWS Transfer Family SFTP server with service-managed users. */
7
31
  export declare class Sftp {
8
- static create(options: SftpConfig & {
9
- slug: string;
10
- environment: EnvironmentType;
11
- }): SftpResources;
32
+ static create(options: SftpCreateOptions): SftpResources;
12
33
  }
package/dist/types.d.ts CHANGED
@@ -984,6 +984,35 @@ export interface SiteConfig {
984
984
  * Example: ['bun install --frozen-lockfile', 'bun run build']
985
985
  */
986
986
  preStart?: string[];
987
+ /**
988
+ * systemd `MemoryHigh` for this site's app unit — the soft limit at which
989
+ * the kernel starts reclaiming the service's own memory, throttling it
990
+ * rather than letting pressure build box-wide.
991
+ *
992
+ * This is the same containment the rpx gateway has carried for a while, and
993
+ * app units are where it was missing. On a shared box a single tenant that
994
+ * leaks takes down every other tenant: memory fills, swap fills, and the
995
+ * kernel's OOM killer starts picking arbitrary victims. Squeezing the
996
+ * offender's own cgroup first keeps the blast radius on the service that
997
+ * actually grew.
998
+ *
999
+ * Accepts systemd size values (`512M`, `2G`, …), or `'infinity'` to opt a
1000
+ * site out. Applies to the app unit only; queue workers have their own
1001
+ * `--memory` restart threshold. @default '2G'
1002
+ */
1003
+ memoryHigh?: string;
1004
+ /**
1005
+ * systemd `MemoryMax` for this site's app unit — the hard limit. Crossing it
1006
+ * invokes the OOM killer INSIDE this service's cgroup, so the unit's
1007
+ * `Restart=always` brings it straight back rather than the kernel killing a
1008
+ * co-tenant.
1009
+ *
1010
+ * Unset by default, deliberately: a hard cap turns a slow leak into a
1011
+ * restart loop for an app that legitimately needs the memory, and the
1012
+ * default has to be safe for workloads nobody has measured. Set it once you
1013
+ * know a site's real ceiling. Should be higher than {@link memoryHigh}.
1014
+ */
1015
+ memoryMax?: string;
987
1016
  /**
988
1017
  * SSR only. tar `--exclude` patterns applied when packaging the release
989
1018
  * tarball. Keep host-specific / heavy paths out of the artifact — most
@@ -1507,13 +1536,65 @@ export interface CdnConfig {
1507
1536
  customDomain?: string;
1508
1537
  certificateArn?: string;
1509
1538
  }
1539
+ /**
1540
+ * POSIX identity a user is mapped to on an EFS-backed server.
1541
+ * Files the user writes are owned by this uid/gid.
1542
+ */
1543
+ export interface SftpPosixProfile {
1544
+ uid: number;
1545
+ gid: number;
1546
+ secondaryGids?: number[];
1547
+ }
1548
+ /** Files live in an S3 bucket. */
1549
+ export interface SftpS3Storage {
1550
+ type: 's3';
1551
+ /** Physical bucket name of an existing bucket. */
1552
+ bucket?: string;
1553
+ /**
1554
+ * Key from `infrastructure.storage`. ts-cloud points the server at the
1555
+ * bucket it generates for that entry, so the bucket is created with the stack.
1556
+ */
1557
+ storageBucket?: string;
1558
+ }
1559
+ /**
1560
+ * Files live on an EFS file system attached to the server, so users get a real
1561
+ * POSIX filesystem (directories, renames, symlinks) instead of object storage.
1562
+ */
1563
+ export interface SftpEfsStorage {
1564
+ type: 'efs';
1565
+ /**
1566
+ * Directory served on box providers (Hetzner, local), where storage on the
1567
+ * server is a directory rather than an EFS file system.
1568
+ * Defaults to `/var/sftp/<slug>`.
1569
+ */
1570
+ path?: string;
1571
+ /** File system ID (`fs-…`) of an existing EFS file system. */
1572
+ fileSystemId?: string;
1573
+ /**
1574
+ * Key from `infrastructure.fileSystem`. ts-cloud points the server at the
1575
+ * file system it generates for that entry, so storage is created with the stack.
1576
+ */
1577
+ fileSystem?: string;
1578
+ /** Default POSIX identity for users that do not set their own. */
1579
+ posixProfile?: SftpPosixProfile;
1580
+ }
1581
+ export type SftpStorageConfig = SftpS3Storage | SftpEfsStorage;
1582
+ export interface SftpUserConfig {
1583
+ sshPublicKeys: string[];
1584
+ homeDirectory?: string;
1585
+ roleArn?: string;
1586
+ /** POSIX identity for this user. Required on EFS-backed servers. */
1587
+ posixProfile?: SftpPosixProfile;
1588
+ }
1510
1589
  export interface SftpConfig {
1511
- bucket: string;
1512
- users: Record<string, {
1513
- sshPublicKeys: string[];
1514
- homeDirectory?: string;
1515
- roleArn?: string;
1516
- }>;
1590
+ /**
1591
+ * Where uploaded files are stored: an EFS file system on the server
1592
+ * (`{ type: 'efs' }`) or an S3 bucket (`{ type: 's3' }`).
1593
+ */
1594
+ storage?: SftpStorageConfig;
1595
+ /** Shorthand for `storage: { type: 's3', bucket }`. */
1596
+ bucket?: string;
1597
+ users: Record<string, SftpUserConfig>;
1517
1598
  endpointType?: 'PUBLIC' | 'VPC';
1518
1599
  endpointDetails?: {
1519
1600
  vpcId: string;
@@ -1523,6 +1604,17 @@ export interface SftpConfig {
1523
1604
  };
1524
1605
  securityPolicyName?: string;
1525
1606
  logging?: boolean;
1607
+ /**
1608
+ * Port the server listens on. Box providers only — AWS Transfer Family is
1609
+ * always reached on port 22. Defaults to 2222, since sshd owns 22 on a box.
1610
+ */
1611
+ port?: number;
1612
+ /** Reject every write. Box providers only. */
1613
+ readOnly?: boolean;
1614
+ /** ts-sftp version installed on the box. Defaults to the latest release. */
1615
+ version?: string;
1616
+ /** System account the box server runs as. Defaults to `ts-sftp`. */
1617
+ serviceUser?: string;
1526
1618
  }
1527
1619
  export interface DnsConfig {
1528
1620
  domain?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ts-cloud/core",
3
3
  "type": "module",
4
- "version": "0.7.119",
4
+ "version": "0.7.121",
5
5
  "description": "Core CloudFormation generation library for ts-cloud",
6
6
  "author": "Chris Breuer <chris@stacksjs.com>",
7
7
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "typecheck": "tsc --noEmit"
32
32
  },
33
33
  "dependencies": {
34
- "@ts-cloud/aws-types": "0.7.119"
34
+ "@ts-cloud/aws-types": "0.7.121"
35
35
  },
36
36
  "devDependencies": {
37
37
  "typescript": "^7.0.2"