@artinstack/migrator 0.1.17 → 0.1.19

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 CHANGED
@@ -4,6 +4,15 @@ Stateless content normalizer and migration framework for transforming **WordPres
4
4
 
5
5
  Portable parsers and JSON export are useful without any specific host. Job orchestration, credentials, and UI are implemented separately via `MigrationSink`.
6
6
 
7
+ ## Used by [ArtInStack](https://www.artinstack.com) (as an optional host)
8
+
9
+ **MIT-licensed OSS** — use the CLI, export normalized JSON, or implement `MigrationSink` for **any** CMS or product. No ArtInStack account required.
10
+
11
+ [ArtInStack](https://www.artinstack.com) is one production integration: the creative operating system for photographers and studios (websites, portfolios, client delivery, commerce, and courses). The platform’s import UI runs this package under the hood.
12
+
13
+ - **Any host** — parsers, normalizer DTOs, conflict analysis, `MigrationSink` contract, and `artinstack-migrate` CLI (dry-run, filesystem export, custom sinks).
14
+ - **ArtInStack product** — workspace owners use **Settings → Import Site**: scan WordPress WXR, preview entities, confirm import, review reports. [Customer docs](https://www.artinstack.com/docs/platform-migration/platform-migration-overview) cover export formats, SEO redirects, and cutover.
15
+
7
16
  See [docs/architecture.md](./docs/architecture.md) for the high-level blueprint: data flow, DTOs, sink contract, and source mappings.
8
17
 
9
18
  ## Package layout
@@ -192,7 +201,7 @@ pnpm dev # watch build
192
201
  | CLI + filesystem JSON export | Yes | No |
193
202
  | `MigrationSink` interface | Yes | Implementation |
194
203
  | Map `data-wp-widget` stubs → platform blocks; populate slider heroes from hints | No | Yes |
195
- | Jobs, worker, credentials, UI | No | Yes |
204
+ | Jobs, worker, credentials, UI | No | Your host — [example: ArtInStack Import Site](https://www.artinstack.com/docs/platform-migration/platform-migration-overview) |
196
205
 
197
206
  ## License
198
207
 
@@ -607,9 +607,11 @@ var SMUGMUG_OAUTH_ENDPOINTS = {
607
607
  authorize: "https://api.smugmug.com/services/oauth/1.0a/authorize",
608
608
  accessToken: "https://api.smugmug.com/services/oauth/1.0a/getAccessToken"
609
609
  };
610
- var smugMugCredentialsSchema = z.object({
610
+ var smugMugConsumerCredentialsSchema = z.object({
611
611
  consumerKey: z.string().min(1),
612
- consumerSecret: z.string().min(1),
612
+ consumerSecret: z.string().min(1)
613
+ });
614
+ var smugMugCredentialsSchema = smugMugConsumerCredentialsSchema.extend({
613
615
  accessToken: z.string().min(1),
614
616
  accessTokenSecret: z.string().min(1)
615
617
  });
@@ -680,23 +682,28 @@ function signSmugMugOAuthRequest(input) {
680
682
  oauthPercentEncode(normalizeRequestUrl(url)),
681
683
  oauthPercentEncode(parameterString)
682
684
  ].join("&");
683
- const signingKey = `${oauthPercentEncode(input.credentials.consumerSecret)}&${oauthPercentEncode(input.credentials.accessTokenSecret)}`;
685
+ const tokenSecret = input.credentials.accessTokenSecret ?? "";
686
+ const signingKey = `${oauthPercentEncode(input.credentials.consumerSecret)}&${oauthPercentEncode(tokenSecret)}`;
684
687
  return createHmac("sha1", signingKey).update(signatureBase).digest("base64");
685
688
  }
686
- function buildOAuthParams(credentials, nonce, timestamp) {
687
- return {
689
+ function buildOAuthParams(credentials, nonce, timestamp, extras) {
690
+ const params = {
688
691
  oauth_consumer_key: credentials.consumerKey,
689
- oauth_token: credentials.accessToken,
690
692
  oauth_signature_method: "HMAC-SHA1",
691
693
  oauth_timestamp: timestamp,
692
694
  oauth_nonce: nonce,
693
- oauth_version: "1.0"
695
+ oauth_version: "1.0",
696
+ ...extras ?? {}
694
697
  };
698
+ if (credentials.accessToken) {
699
+ params.oauth_token = credentials.accessToken;
700
+ }
701
+ return params;
695
702
  }
696
703
  function buildSmugMugAuthorizationHeader(input) {
697
704
  const nonce = input.nonce ?? randomBytes(16).toString("hex");
698
705
  const timestamp = input.timestamp ?? String(Math.floor(Date.now() / 1e3));
699
- const oauthParams = buildOAuthParams(input.credentials, nonce, timestamp);
706
+ const oauthParams = buildOAuthParams(input.credentials, nonce, timestamp, input.oauthExtras);
700
707
  const signature = signSmugMugOAuthRequest({
701
708
  method: input.method,
702
709
  url: input.url,
@@ -708,6 +715,141 @@ function buildSmugMugAuthorizationHeader(input) {
708
715
  const headerValue = Object.keys(headerParams).sort().map((key) => `${oauthPercentEncode(key)}="${oauthPercentEncode(headerParams[key])}"`).join(", ");
709
716
  return `OAuth ${headerValue}`;
710
717
  }
718
+ function parseSmugMugOAuthFormBody(body) {
719
+ const params = new URLSearchParams(body.trim());
720
+ const out = {};
721
+ params.forEach((value, key) => {
722
+ out[key] = value;
723
+ });
724
+ return out;
725
+ }
726
+ function requireOAuthFormField(fields, name) {
727
+ const value = fields[name]?.trim();
728
+ if (!value) {
729
+ throw new Error(`SmugMug OAuth response missing ${name}`);
730
+ }
731
+ return value;
732
+ }
733
+ async function postSmugMugOAuthTokenEndpoint(input) {
734
+ const fetchImpl = input.fetchImpl ?? fetch;
735
+ const authorization = buildSmugMugAuthorizationHeader({
736
+ method: "POST",
737
+ url: input.url,
738
+ credentials: input.credentials,
739
+ oauthExtras: input.oauthExtras,
740
+ nonce: input.nonce,
741
+ timestamp: input.timestamp
742
+ });
743
+ const response = await fetchImpl(input.url, {
744
+ method: "POST",
745
+ headers: {
746
+ Accept: "application/x-www-form-urlencoded",
747
+ Authorization: authorization
748
+ }
749
+ });
750
+ const body = await response.text();
751
+ if (!response.ok) {
752
+ throw new Error(
753
+ `SmugMug OAuth HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`
754
+ );
755
+ }
756
+ return parseSmugMugOAuthFormBody(body);
757
+ }
758
+ async function getSmugMugRequestToken(input) {
759
+ const consumer = smugMugConsumerCredentialsSchema.parse(input.consumer);
760
+ const callbackUrl = input.callbackUrl.trim();
761
+ if (!callbackUrl) {
762
+ throw new Error('SmugMug OAuth callbackUrl is required (use a URL or "oob")');
763
+ }
764
+ const fields = await postSmugMugOAuthTokenEndpoint({
765
+ url: SMUGMUG_OAUTH_ENDPOINTS.requestToken,
766
+ credentials: {
767
+ consumerKey: consumer.consumerKey,
768
+ consumerSecret: consumer.consumerSecret,
769
+ accessTokenSecret: ""
770
+ },
771
+ oauthExtras: { oauth_callback: callbackUrl },
772
+ fetchImpl: input.fetchImpl,
773
+ nonce: input.nonce,
774
+ timestamp: input.timestamp
775
+ });
776
+ return {
777
+ token: requireOAuthFormField(fields, "oauth_token"),
778
+ tokenSecret: requireOAuthFormField(fields, "oauth_token_secret"),
779
+ callbackConfirmed: fields.oauth_callback_confirmed === "true"
780
+ };
781
+ }
782
+ function buildSmugMugAuthorizeUrl(options) {
783
+ const requestToken = options.requestToken.trim();
784
+ if (!requestToken) {
785
+ throw new Error("SmugMug authorize URL requires requestToken");
786
+ }
787
+ const url = new URL(SMUGMUG_OAUTH_ENDPOINTS.authorize);
788
+ url.searchParams.set("oauth_token", requestToken);
789
+ url.searchParams.set("Access", options.access ?? "Full");
790
+ url.searchParams.set("Permissions", options.permissions ?? "Modify");
791
+ if (options.allowThirdPartyLogin !== void 0) {
792
+ url.searchParams.set("allowThirdPartyLogin", options.allowThirdPartyLogin ? "1" : "0");
793
+ }
794
+ if (options.showSignUpButton !== void 0) {
795
+ url.searchParams.set("showSignUpButton", options.showSignUpButton ? "true" : "false");
796
+ }
797
+ if (options.username?.trim()) {
798
+ url.searchParams.set("username", options.username.trim());
799
+ }
800
+ return url.toString();
801
+ }
802
+ async function getSmugMugAccessToken(input) {
803
+ const consumer = smugMugConsumerCredentialsSchema.parse(input.consumer);
804
+ const token = input.requestToken.token.trim();
805
+ const tokenSecret = input.requestToken.tokenSecret;
806
+ const verifier = input.verifier.trim();
807
+ if (!token || tokenSecret === void 0 || tokenSecret === null) {
808
+ throw new Error("SmugMug access-token exchange requires request token + secret");
809
+ }
810
+ if (!verifier) {
811
+ throw new Error("SmugMug access-token exchange requires oauth_verifier");
812
+ }
813
+ const fields = await postSmugMugOAuthTokenEndpoint({
814
+ url: SMUGMUG_OAUTH_ENDPOINTS.accessToken,
815
+ credentials: {
816
+ consumerKey: consumer.consumerKey,
817
+ consumerSecret: consumer.consumerSecret,
818
+ accessToken: token,
819
+ accessTokenSecret: tokenSecret
820
+ },
821
+ oauthExtras: { oauth_verifier: verifier },
822
+ fetchImpl: input.fetchImpl,
823
+ nonce: input.nonce,
824
+ timestamp: input.timestamp
825
+ });
826
+ return {
827
+ token: requireOAuthFormField(fields, "oauth_token"),
828
+ tokenSecret: requireOAuthFormField(fields, "oauth_token_secret")
829
+ };
830
+ }
831
+ async function createSmugMugOAuthSession(input) {
832
+ const request = await getSmugMugRequestToken({
833
+ consumer: {
834
+ consumerKey: input.consumerKey,
835
+ consumerSecret: input.consumerSecret
836
+ },
837
+ callbackUrl: input.callbackUrl,
838
+ fetchImpl: input.fetchImpl,
839
+ nonce: input.nonce,
840
+ timestamp: input.timestamp
841
+ });
842
+ return {
843
+ requestToken: request.token,
844
+ requestTokenSecret: request.tokenSecret,
845
+ authorizeUrl: buildSmugMugAuthorizeUrl({
846
+ requestToken: request.token,
847
+ access: input.access,
848
+ permissions: input.permissions
849
+ }),
850
+ callbackConfirmed: request.callbackConfirmed
851
+ };
852
+ }
711
853
  function readSmugMugCredentialsFromEnv(env = process.env) {
712
854
  return smugMugCredentialsSchema.parse({
713
855
  consumerKey: env.SMUGMUG_CONSUMER_KEY,
@@ -719,6 +861,14 @@ function readSmugMugCredentialsFromEnv(env = process.env) {
719
861
  function sleep(ms) {
720
862
  return new Promise((resolve) => setTimeout(resolve, ms));
721
863
  }
864
+ function resolveSmugMugUriLink(link, label) {
865
+ if (typeof link === "string" && link.trim()) return link.trim();
866
+ if (link && typeof link === "object") {
867
+ const uri = link.Uri;
868
+ if (typeof uri === "string" && uri.trim()) return uri.trim();
869
+ }
870
+ throw new Error(`SmugMug response missing ${label} URI`);
871
+ }
722
872
  function albumKeyFromUri(uri) {
723
873
  const match = uri.match(/\/album\/([^/?!]+)/i);
724
874
  if (!match?.[1]) {
@@ -769,7 +919,10 @@ var SmugMugApiClient = class {
769
919
  /** Validate credentials against `GET /user/!authuser`. */
770
920
  async validateCredentials() {
771
921
  const user = await this.getAuthUser();
772
- return { nick: user.NickName, rootNodeUri: user.Uris.Node };
922
+ return {
923
+ nick: user.NickName,
924
+ rootNodeUri: resolveSmugMugUriLink(user.Uris.Node, "User.Uris.Node")
925
+ };
773
926
  }
774
927
  /** Crawl the authenticated user's node tree into flat export tables for `parse-node.ts`. */
775
928
  async crawlExport() {
@@ -777,7 +930,8 @@ var SmugMugApiClient = class {
777
930
  const folders = [];
778
931
  const albums = [];
779
932
  const images = [];
780
- await this.walkNode(user.Uris.Node, void 0, folders, albums, images);
933
+ const rootNodeUri = resolveSmugMugUriLink(user.Uris.Node, "User.Uris.Node");
934
+ await this.walkNode(rootNodeUri, void 0, folders, albums, images);
781
935
  return {
782
936
  exportVersion: 1,
783
937
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -787,7 +941,12 @@ var SmugMugApiClient = class {
787
941
  };
788
942
  }
789
943
  async getAuthUser() {
790
- const envelope = await this.requestJson(`${SMUGMUG_API_BASE}/user/!authuser`);
944
+ const envelope = await this.requestJson(
945
+ `${SMUGMUG_API_BASE}/user/!authuser`
946
+ );
947
+ if (envelope.Response.User?.Uris?.Node != null) {
948
+ return envelope.Response.User;
949
+ }
791
950
  return envelope.Response;
792
951
  }
793
952
  async walkNode(nodeUri, parentFolderId, folders, albums, images) {
@@ -814,8 +973,8 @@ var SmugMugApiClient = class {
814
973
  description: child.Description,
815
974
  url: child.WebUri
816
975
  });
817
- const albumUri = child.Uris?.Album;
818
- if (albumUri) {
976
+ if (child.Uris?.Album != null) {
977
+ const albumUri = resolveSmugMugUriLink(child.Uris.Album, "Node.Uris.Album");
819
978
  await this.collectAlbumImages(albumUri, child.NodeID, images);
820
979
  }
821
980
  }
@@ -2557,10 +2716,17 @@ export {
2557
2716
  wordpressAdapter,
2558
2717
  SMUGMUG_API_BASE,
2559
2718
  SMUGMUG_OAUTH_ENDPOINTS,
2719
+ smugMugConsumerCredentialsSchema,
2560
2720
  smugMugCredentialsSchema,
2561
2721
  signSmugMugOAuthRequest,
2562
2722
  buildSmugMugAuthorizationHeader,
2723
+ parseSmugMugOAuthFormBody,
2724
+ getSmugMugRequestToken,
2725
+ buildSmugMugAuthorizeUrl,
2726
+ getSmugMugAccessToken,
2727
+ createSmugMugOAuthSession,
2563
2728
  readSmugMugCredentialsFromEnv,
2729
+ resolveSmugMugUriLink,
2564
2730
  SmugMugApiClient,
2565
2731
  smugmugAdapter,
2566
2732
  squarespaceAdapter,
@@ -2569,4 +2735,4 @@ export {
2569
2735
  wixAdapter,
2570
2736
  getAdapter
2571
2737
  };
2572
- //# sourceMappingURL=chunk-RLMYUUNK.js.map
2738
+ //# sourceMappingURL=chunk-5RKHB6GI.js.map